mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-01 10:41:16 -05:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5c68c6c6e | ||
|
|
58c39815e4 | ||
|
|
4b706f466f | ||
|
|
19f72b1386 | ||
|
|
b4d883dbf0 | ||
|
|
86f8f4ebdf | ||
|
|
b5df1ed8dd | ||
|
|
b2c25db5d9 | ||
|
|
c0c876c694 | ||
|
|
b832d19e0e | ||
|
|
68214becad | ||
|
|
0971922518 | ||
|
|
1e9767b0bb | ||
|
|
3f12bdad9d | ||
|
|
0ee17cc0ee | ||
|
|
c7448f7e99 | ||
|
|
835b350d53 | ||
|
|
b7cbecc61d | ||
|
|
5e2f950b7e | ||
|
|
9a97a904fb | ||
|
|
56b6753320 | ||
|
|
f7675c0279 | ||
|
|
961d237d42 | ||
|
|
47c2ae1e56 | ||
|
|
9658a40c76 | ||
|
|
752ddaea9c | ||
|
|
5efc277316 | ||
|
|
88b32efa97 | ||
|
|
03f692a62f | ||
|
|
bca8ffe676 | ||
|
|
d2590f4222 | ||
|
|
ef245b2566 | ||
|
|
9ae92962d3 | ||
|
|
e52cd927a5 | ||
|
|
582f7c2ebc | ||
|
|
ce5e5df644 | ||
|
|
6a2e663c57 | ||
|
|
f6adb93518 | ||
|
|
077a4fb3ee | ||
|
|
dc4fa1b487 | ||
|
|
949b51defd | ||
|
|
c2b824c31e | ||
|
|
cc846830fe | ||
|
|
f6ab23fa03 | ||
|
|
44d84187c8 | ||
|
|
fe78524e41 | ||
|
|
adc0e8227f | ||
|
|
55cb24be68 | ||
|
|
8efc021bd7 | ||
|
|
b649bdeb2e | ||
|
|
af4ca2e018 | ||
|
|
1fa9606491 | ||
|
|
7620fa8186 | ||
|
|
4a5d42d65b | ||
|
|
af0f582090 | ||
|
|
4f91ae7f1c | ||
|
|
67c4b55cbb | ||
|
|
7ff608b08c | ||
|
|
4ebbea7825 | ||
|
|
1260e94199 | ||
|
|
3b8d0d3a8a | ||
|
|
2725646a6a | ||
|
|
89cddcc626 | ||
|
|
f7d9d2a47c | ||
|
|
60833efcda | ||
|
|
70208eb81a | ||
|
|
ae6e734dc9 | ||
|
|
f1fc2a5f96 | ||
|
|
b62621c9c6 | ||
|
|
a372348dbf | ||
|
|
779d2a6b43 | ||
|
|
9510c0232f | ||
|
|
1e97960eab | ||
|
|
c756156e0d | ||
|
|
af98a252c8 | ||
|
|
a7f016d73f | ||
|
|
3a287ebc77 | ||
|
|
65c1a60447 | ||
|
|
c6906c8caf | ||
|
|
ace1bd7b0f | ||
|
|
56e82cd046 | ||
|
|
58d6b4c67c | ||
|
|
7e4a0f6e07 | ||
|
|
b543696fa9 | ||
|
|
e669738e38 | ||
|
|
961977c9e2 | ||
|
|
e3d2bec203 | ||
|
|
75d9249577 | ||
|
|
016a7e7559 | ||
|
|
b6e7a2e77a | ||
|
|
fd9e62591e | ||
|
|
fd485b979c | ||
|
|
410e845811 | ||
|
|
b5207d97fb | ||
|
|
3122dc4807 | ||
|
|
e010f0f57b | ||
|
|
864a7630d5 | ||
|
|
b603a177e2 | ||
|
|
ee2fd9f9ae | ||
|
|
a14066c43f | ||
|
|
1bcd088782 | ||
|
|
4ff937feec | ||
|
|
77d49c52f0 | ||
|
|
f09cfecb13 | ||
|
|
8655f15731 | ||
|
|
d629ffb6e5 | ||
|
|
21e0ad5017 | ||
|
|
279a1f2ab2 | ||
|
|
957be55927 | ||
|
|
63a8be657c | ||
|
|
7559f0aff4 | ||
|
|
c89afa613f | ||
|
|
7f449694c8 | ||
|
|
8797b3b360 | ||
|
|
4af333e22d | ||
|
|
17e8b6c16c | ||
|
|
694f1d5e8f | ||
|
|
6f32692342 | ||
|
|
358d838f3b | ||
|
|
2e47486195 | ||
|
|
6936d4da3b | ||
|
|
17a7a57136 | ||
|
|
a3552471af | ||
|
|
886208460b | ||
|
|
a6fea3a60a | ||
|
|
fb9c2e1494 | ||
|
|
2b259eee0c | ||
|
|
d9a8e671a1 | ||
|
|
f9a9cb83c4 | ||
|
|
3eae4b478f | ||
|
|
06dc2eadae | ||
|
|
2fa11dab67 | ||
|
|
c73e3a489c |
@@ -26,6 +26,9 @@ HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
|
||||
# Whether to force redirect all HTTP traffic (80) to HTTPS (443). Defaults to true.
|
||||
FORCE_HTTPS_REDIRECT=true
|
||||
|
||||
# ===========================================
|
||||
# EMAIL SERVER CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
open_collective: aliasvault
|
||||
|
||||
8
.github/workflows/docker-build.yml
vendored
8
.github/workflows/docker-build.yml
vendored
@@ -109,8 +109,8 @@ jobs:
|
||||
echo "🔧 Testing admin password reset flow..."
|
||||
|
||||
# Run the reset password script with auto-confirm
|
||||
echo "Running reset-admin-password.sh script..."
|
||||
password_output=$(docker exec aliasvault-test reset-admin-password.sh -y 2>&1)
|
||||
echo "Running reset-admin-password command..."
|
||||
password_output=$(docker exec aliasvault-test aliasvault reset-admin-password -y 2>&1)
|
||||
echo "Script output:"
|
||||
echo "$password_output"
|
||||
|
||||
@@ -174,10 +174,10 @@ jobs:
|
||||
- name: Check local docker-compose.yml for :latest tags
|
||||
run: |
|
||||
# Check for explicit version tags instead of :latest
|
||||
if grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
if grep -E "ghcr\.io/aliasvault/[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
echo "❌ Error: docker-compose.yml contains explicit version tags instead of :latest"
|
||||
echo "Found the following explicit versions:"
|
||||
grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
grep -E "ghcr\.io/aliasvault/[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
echo ""
|
||||
echo "All AliasVault images in docker-compose.yml must use ':latest' tags, not explicit versions."
|
||||
echo "Please update docker-compose.yml to use ':latest' for all AliasVault images."
|
||||
|
||||
252
.github/workflows/release.yml
vendored
252
.github/workflows/release.yml
vendored
@@ -26,10 +26,6 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
upload-install-script:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -127,123 +123,253 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for multi-container images
|
||||
id: meta
|
||||
- name: Extract metadata for Postgres image
|
||||
id: postgres-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
|
||||
images: ghcr.io/aliasvault/postgres
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault PostgreSQL
|
||||
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Generate tags for containers
|
||||
id: tags
|
||||
run: |
|
||||
# Transform base tags to include suffixes for each container
|
||||
TAGS="${{ steps.meta.outputs.tags }}"
|
||||
- name: Extract metadata for API image
|
||||
id: api-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/api
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault API
|
||||
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Client image
|
||||
id: client-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/client
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Client
|
||||
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Admin image
|
||||
id: admin-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/admin
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Admin
|
||||
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Reverse Proxy image
|
||||
id: reverse-proxy-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/reverse-proxy
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Reverse Proxy
|
||||
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for SMTP image
|
||||
id: smtp-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/smtp
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault SMTP Service
|
||||
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for TaskRunner image
|
||||
id: task-runner-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/task-runner
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault TaskRunner
|
||||
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for InstallCLI image
|
||||
id: installcli-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/installcli
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Install CLI
|
||||
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
|
||||
|
||||
# Generate tags for each container by replacing the base image name with suffixed versions
|
||||
echo "postgres=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-postgres|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "api=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-api|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "client=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-client|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "admin=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-admin|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "reverse-proxy=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-reverse-proxy|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "smtp=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-smtp|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "task-runner=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-task-runner|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
echo "installcli=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-installcli|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Postgres image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Databases/AliasServerDb/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.postgres }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.postgres-meta.outputs.tags }}
|
||||
labels: ${{ steps.postgres-meta.outputs.labels }}
|
||||
annotations: ${{ steps.postgres-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push API image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.api }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.api-meta.outputs.tags }}
|
||||
labels: ${{ steps.api-meta.outputs.labels }}
|
||||
annotations: ${{ steps.api-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Client image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.client }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.client-meta.outputs.tags }}
|
||||
labels: ${{ steps.client-meta.outputs.labels }}
|
||||
annotations: ${{ steps.client-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Admin image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Admin/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.admin }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.admin-meta.outputs.tags }}
|
||||
labels: ${{ steps.admin-meta.outputs.labels }}
|
||||
annotations: ${{ steps.admin-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Reverse Proxy image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.reverse-proxy }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.reverse-proxy-meta.outputs.tags }}
|
||||
labels: ${{ steps.reverse-proxy-meta.outputs.labels }}
|
||||
annotations: ${{ steps.reverse-proxy-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push SMTP image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Services/AliasVault.SmtpService/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.smtp }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.smtp-meta.outputs.tags }}
|
||||
labels: ${{ steps.smtp-meta.outputs.labels }}
|
||||
annotations: ${{ steps.smtp-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push TaskRunner image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Services/AliasVault.TaskRunner/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.task-runner }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.task-runner-meta.outputs.tags }}
|
||||
labels: ${{ steps.task-runner-meta.outputs.labels }}
|
||||
annotations: ${{ steps.task-runner-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push InstallCli image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.installcli }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.installcli-meta.outputs.tags }}
|
||||
labels: ${{ steps.installcli-meta.outputs.labels }}
|
||||
annotations: ${{ steps.installcli-meta.outputs.annotations }}
|
||||
|
||||
build-and-push-docker-all-in-one:
|
||||
if: github.event_name == 'release' || inputs.build_all_in_one
|
||||
@@ -262,10 +388,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -282,25 +404,35 @@ jobs:
|
||||
- name: Extract metadata for all-in-one image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ env.REPO_LOWER }}
|
||||
ghcr.io/aliasvault/aliasvault
|
||||
aliasvault/aliasvault
|
||||
tags: |
|
||||
# For release events with latest tag (only for non-prerelease)
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
# semver tags for releases (works for prerelease and normal release)
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
# For tags, use tag name
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
# For branches, use branch name and branch name + short SHA for uniqueness
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault All-in-One
|
||||
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
|
||||
|
||||
- name: Build and push all-in-one image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: dockerfiles/all-in-one/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
80
.github/workflows/sonarcloud-code-analysis.yml
vendored
80
.github/workflows/sonarcloud-code-analysis.yml
vendored
@@ -1,80 +0,0 @@
|
||||
# This workflow will perform a SonarCloud code analysis on every push to the main branch or
|
||||
# when a pull request is opened, synchronized, or reopened. The "pull_request_target" event is
|
||||
# used to ensure that the analysis is done on the source branch of the pull request which has
|
||||
# access to the SonarCloud token secret.
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
# Cancel in-progress jobs when new commits are pushed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Install WASM workload
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Checkout code of PR branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
|
||||
- name: Build and analyze
|
||||
working-directory: apps/server
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
$scanner = "${{ github.workspace }}\.sonar\scanner\dotnet-sonarscanner"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
} else {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
& $scanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -404,6 +404,7 @@ certificates/**/*.crt
|
||||
certificates/**/*.key
|
||||
certificates/**/*.pfx
|
||||
certificates/**/*.pem
|
||||
certificates/**/.hostname_marker
|
||||
certificates/letsencrypt/**
|
||||
|
||||
# Secrets
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"label": "Build and watch Docs",
|
||||
"type": "shell",
|
||||
"command": "docker compose up",
|
||||
"command": "docker compose build && docker compose up",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
37
README.md
37
README.md
@@ -1,9 +1,8 @@
|
||||
# <img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="35" alt="AliasVault"> AliasVault
|
||||
AliasVault is a privacy-first password and email alias manager. Create unique identities, strong passwords, and random email aliases for every website you use. Fully end-to-end encrypted, with a built-in email server and zero third-party dependencies.
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github&label=Release">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=Sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/github/v/release/aliasvault/aliasvault?include_prereleases&logo=github&label=Release">](https://github.com/aliasvault/aliasvault/releases)
|
||||
[](https://github.com/aliasvault/aliasvault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
[<img src="https://badges.crowdin.net/aliasvault/localized.svg">](https://crowdin.com/project/aliasvault)
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=Discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
@@ -66,33 +65,31 @@ AliasVault is available on:
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
## Self-hosting
|
||||
For full control over your own data you can self-host and install AliasVault on your own servers.
|
||||
> [!NOTE]
|
||||
> **Requirements:** 1 vCPU, 1GB RAM, 16GB disk, Docker ≥ 20.10, 64-bit Linux
|
||||
|
||||
### Install using install script
|
||||
AliasVault can be self-hosted on your own servers using two different installation methods. Both use Docker, but they differ in how much is automated versus how much you manage yourself.
|
||||
|
||||
This method uses pre-built Docker images and works on minimal hardware specifications:
|
||||
- **Option 1: Install Script** - Managed solution with automatic SSL (recommended for VPS/cloud)
|
||||
|
||||
- 64-bit Linux VM (Ubuntu/AlmaLinux) or Raspberry Pi, with root access
|
||||
- Minimum: 1 vCPU, 1GB RAM, 16GB disk
|
||||
- Docker ≥ 20.10 and Docker Compose ≥ 2.0
|
||||
- **Option 2: Docker Compose** - Single container with manual setup for use with existing SSL infrastructure (NAS, homelab)
|
||||
|
||||
### Quick Start (Install Script)
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
curl -L -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/install.sh
|
||||
# Download and run install script
|
||||
curl -L -o install.sh https://github.com/aliasvault/aliasvault/releases/latest/download/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
|
||||
./install.sh install
|
||||
```
|
||||
|
||||
The install script will output the URL where the app is available. By default this is:
|
||||
- Client: https://localhost
|
||||
- Admin portal: https://localhost/admin
|
||||
For other installation methods and more detailed steps, please read the [full installation guide](https://docs.aliasvault.net/installation) in the official docs.
|
||||
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
|
||||
|
||||
## Technical documentation
|
||||
## Documentation
|
||||
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
|
||||
|
||||
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
|
||||
|
||||
## Security Architecture
|
||||
@@ -133,12 +130,12 @@ Core features that are being worked on:
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/lanedirt/AliasVault/issues/731)
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/aliasvault/aliasvault/issues/731)
|
||||
|
||||
### Got feedback or ideas?
|
||||
Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)! Contributions are warmly welcomed—whether in feature development, testing, or spreading the word. Get in touch on Discord if you're interested in contributing.
|
||||
Feel free to open an issue or discussion on GitHub. We warmly welcome all contributions: whether it’s translating, testing, helping to build features, sharing feedback - or helping spread the word about AliasVault. Every bit of support helps the project grow, so don’t hesitate to jump in and [say hi to us on Discord](https://discord.gg/DsaXMTEtpF)!
|
||||
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
AliasVault is open-source and community-driven. If you like what we’re building, consider supporting us through [Open Collective](https://opencollective.com/aliasvault) or through:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 50px !important;" ></a>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<SonarLint>
|
||||
<Rules>
|
||||
<Rule>
|
||||
<Key>S1135</Key>
|
||||
<Parameters>
|
||||
<Parameter>
|
||||
<Name>sonarlint.rule.enabled</Name>
|
||||
<Value>false</Value>
|
||||
</Parameter>
|
||||
</Parameters>
|
||||
</Rule>
|
||||
</Rules>
|
||||
</SonarLint>
|
||||
10
apps/browser-extension/package-lock.json
generated
10
apps/browser-extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.21.2",
|
||||
"version": "0.22.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.21.2",
|
||||
"version": "0.22.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
@@ -13064,9 +13064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
<path d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z" fill="#EEC170"/>
|
||||
<path d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z" fill="#EEC170"/>
|
||||
<path d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z" fill="#EEC170"/>
|
||||
<path d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z" fill="#EEC170"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 220001;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.22.0;
|
||||
MARKETING_VERSION = 0.23.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 220001;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.22.0;
|
||||
MARKETING_VERSION = 0.23.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 220001;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.22.0;
|
||||
MARKETING_VERSION = 0.23.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 220001;
|
||||
CURRENT_PROJECT_VERSION = 230000;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.22.0;
|
||||
MARKETING_VERSION = 0.23.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -108,6 +108,4 @@ async function extendAutoLockTimer(): Promise<void> {
|
||||
console.error('[AUTO_LOCK] Error locking vault:', error);
|
||||
}
|
||||
}, timeout * 1000);
|
||||
|
||||
console.info(`[AUTO_LOCK] Timer extended (popup heartbeat)`);
|
||||
}
|
||||
|
||||
@@ -11,25 +11,25 @@ import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/auth/AuthSettings';
|
||||
import Login from '@/entrypoints/popup/pages/auth/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/auth/Logout';
|
||||
import Unlock from '@/entrypoints/popup/pages/auth/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/auth/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/auth/Upgrade';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/credentials/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/credentials/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/credentials/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/emails/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/emails/EmailsList';
|
||||
import Index from '@/entrypoints/popup/pages/Index';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/Logout';
|
||||
import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
import AutofillSettings from '@/entrypoints/popup/pages/settings/AutofillSettings';
|
||||
import AutoLockSettings from '@/entrypoints/popup/pages/settings/AutoLockSettings';
|
||||
import ClipboardSettings from '@/entrypoints/popup/pages/settings/ClipboardSettings';
|
||||
import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuSettings';
|
||||
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/Upgrade';
|
||||
import Settings from '@/entrypoints/popup/pages/settings/Settings';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={id} className="block font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
@@ -177,7 +177,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
className={`flex-1 min-w-0 px-3 py-2 border ${
|
||||
className={`flex-1 min-w-0 px-3 py-2 border text-sm ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} ${
|
||||
!isCustomDomain ? 'rounded-l-md' : 'rounded-md'
|
||||
@@ -209,9 +209,9 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
{showPrivateDomains && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('credentials.privateEmailTitle')} <span className="text-xs text-gray-500 dark:text-gray-400">({t('credentials.privateEmailAliasVaultServer')})</span>
|
||||
{t('credentials.privateEmailTitle')} <span className="text-gray-500 dark:text-gray-400">({t('credentials.privateEmailAliasVaultServer')})</span>
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-3">
|
||||
{t('credentials.privateEmailDescription')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -213,7 +213,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
}
|
||||
if (emails.length === 0) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4 text-sm">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
|
||||
@@ -109,7 +109,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
const inputClasses = `mt-1 block text-sm w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 py-2 px-3`;
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
clipboardService.setCopied(id);
|
||||
|
||||
|
||||
// Notify background script that clipboard was copied
|
||||
await sendMessage('CLIPBOARD_COPIED', { value }, 'background');
|
||||
|
||||
@@ -111,7 +111,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
onClick={copyToClipboard}
|
||||
className={`w-full px-3 py-2.5 bg-white border ${
|
||||
copied ? 'border-green-500 border-2' : 'border-gray-300'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
} text-gray-900 text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{copied ? (
|
||||
|
||||
@@ -9,7 +9,9 @@ export enum HeaderIconType {
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save',
|
||||
PLUS = 'plus',
|
||||
TAB = 'tab'
|
||||
TAB = 'tab',
|
||||
EYE = 'eye',
|
||||
EYE_OFF = 'eye_off'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
@@ -131,19 +133,7 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V7l-4-4z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 3v5h10"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 12a2 2 0 100 4 2 2 0 000-4z"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
@@ -179,6 +169,44 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.EYE]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.EYE_OFF]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.credentials')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.credentials')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('emails')}
|
||||
@@ -73,7 +73,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.emails')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.emails')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('settings')}
|
||||
@@ -85,7 +85,7 @@ const BottomNav: React.FC = () => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.settings')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.settings')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import Logo from '@/entrypoints/popup/components/Logo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
@@ -87,11 +88,15 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() => logoClick()}
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">{t('common.appName')}</h1>
|
||||
<Logo
|
||||
width={125}
|
||||
height={40}
|
||||
showText={true}
|
||||
className="text-gray-900 dark:text-white"
|
||||
/>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
<span className="text-primary-500 text-[10px] font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const LoginServerInfo: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
({t('auth.connectingTo')}{' '}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
type LogoProps = {
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showText?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logo component.
|
||||
*/
|
||||
const Logo: React.FC<LogoProps> = ({
|
||||
className = '',
|
||||
width = 200,
|
||||
height = 50,
|
||||
showText = true,
|
||||
color = 'currentColor'
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
version="1.1"
|
||||
viewBox="0 0 2000 500"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
>
|
||||
{/* Logo mark */}
|
||||
<path
|
||||
d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
|
||||
{/* Wordmark - only show if showText is true */}
|
||||
{showText && (
|
||||
<text
|
||||
x="550"
|
||||
y="355"
|
||||
fontFamily="Arial, Helvetica, sans-serif"
|
||||
fontWeight="700"
|
||||
fontSize="290"
|
||||
letterSpacing="-7"
|
||||
fill={color}
|
||||
>
|
||||
AliasVault
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -114,7 +114,7 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className="outline-0 text-sm shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -49,7 +49,7 @@ const UsernameField: React.FC<IUsernameFieldProps> = ({
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className="outline-0 text-sm shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -51,9 +51,15 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
const segments = location.pathname.split('/').filter(Boolean);
|
||||
const historyEntries: NavigationHistoryEntry[] = [];
|
||||
|
||||
// Build history entries for each segment
|
||||
let currentPath = '';
|
||||
for (const segment of segments) {
|
||||
currentPath += '/' + segment;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
currentPath += '/' + segments[i];
|
||||
|
||||
/*
|
||||
* For settings subpages, include both /settings and the subpage
|
||||
* For email details, include both /emails and the specific email
|
||||
*/
|
||||
historyEntries.push({
|
||||
pathname: currentPath,
|
||||
search: location.search,
|
||||
|
||||
@@ -51,21 +51,16 @@ const Reinitialize: React.FC = () => {
|
||||
if (lastPage && lastVisitTime) {
|
||||
const timeSinceLastVisit = Date.now() - lastVisitTime;
|
||||
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
|
||||
// Restore the navigation history
|
||||
if (savedHistory?.length) {
|
||||
// First navigate to credentials page as the base
|
||||
navigate('/credentials', { replace: true });
|
||||
|
||||
// Then restore the history stack
|
||||
for (const entry of savedHistory) {
|
||||
navigate(entry.pathname + entry.search + entry.hash);
|
||||
}
|
||||
return;
|
||||
// For nested routes, build up the navigation history properly
|
||||
if (savedHistory?.length > 1) {
|
||||
// Navigate to the base route first
|
||||
navigate(savedHistory[0].pathname, { replace: true });
|
||||
// Then navigate to the final destination
|
||||
navigate(lastPage, { replace: false });
|
||||
} else {
|
||||
// Simple navigation for non-nested routes
|
||||
navigate(lastPage, { replace: true });
|
||||
}
|
||||
|
||||
// Fallback to simple navigation if no history
|
||||
navigate('/credentials', { replace: true });
|
||||
navigate(lastPage, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,13 +176,13 @@ const AuthSettings: React.FC = () => {
|
||||
{/* Language Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="api-connection" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
{t('settings.serverUrl')}
|
||||
</label>
|
||||
<select
|
||||
@@ -201,7 +201,7 @@ const AuthSettings: React.FC = () => {
|
||||
{selectedOption === 'custom' && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-client-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="custom-client-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom client URL
|
||||
</label>
|
||||
<input
|
||||
@@ -217,7 +217,7 @@ const AuthSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="custom-api-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom API URL
|
||||
</label>
|
||||
<input
|
||||
@@ -238,7 +238,7 @@ const AuthSettings: React.FC = () => {
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
@@ -6,13 +6,14 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
@@ -21,8 +22,6 @@ import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/we
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
|
||||
import ConversionUtility from '../utils/ConversionUtility';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
@@ -40,6 +39,7 @@ const Login: React.FC = () => {
|
||||
});
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
|
||||
@@ -362,11 +362,11 @@ const Login: React.FC = () => {
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">{t('auth.loginTitle')}</h2>
|
||||
<LoginServerInfo />
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="username">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="username">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
@@ -377,19 +377,29 @@ const Login: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center">
|
||||
@@ -408,7 +418,7 @@ const Login: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
{t('auth.noAccount')}{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
@@ -35,6 +35,7 @@ const Unlock: React.FC = () => {
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
@@ -144,10 +145,10 @@ const Unlock: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('auth.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -159,32 +160,42 @@ const Unlock: React.FC = () => {
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
<div className="mb-4 text-red-500 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.masterPassword')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
{t('auth.unlockVault')}
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
<div className="font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
{t('auth.switchAccounts')} <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('auth.logout')}</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -239,7 +239,7 @@ const Upgrade: React.FC = () => {
|
||||
title={t('upgrade.alerts.selfHostedServer')}
|
||||
message={t('upgrade.alerts.selfHostedWarning')}
|
||||
confirmText={t('upgrade.alerts.continueUpgrade')}
|
||||
cancelText={t('upgrade.alerts.cancel')}
|
||||
cancelText={t('common.cancel')}
|
||||
/>
|
||||
|
||||
{/* Version info modal */}
|
||||
@@ -253,7 +253,7 @@ const Upgrade: React.FC = () => {
|
||||
|
||||
<form className="w-full px-2 pt-2 pb-2 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
<div className="mb-4 text-red-500 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +268,7 @@ const Upgrade: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{username}
|
||||
</p>
|
||||
</div>
|
||||
@@ -277,12 +277,12 @@ const Upgrade: React.FC = () => {
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">{t('upgrade.title')}</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
|
||||
<p className="text-gray-700 dark:text-gray-200 mb-4">
|
||||
{t('upgrade.subtitle')}
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showVersionDialog}
|
||||
@@ -536,8 +536,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('random')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'random' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'random' ? 'bg-primary-500 text-white font-medium' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -553,8 +553,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('manual')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'manual' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'manual' ? 'bg-primary-500 text-white font-medium' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -630,7 +630,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 flex items-center justify-center gap-2"
|
||||
className="w-full text-sm bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
@@ -188,10 +188,10 @@ const CredentialsList: React.FC = () => {
|
||||
|
||||
{credentials.length === 0 ? (
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
|
||||
<p className="text-sm">
|
||||
<p>
|
||||
{t('credentials.welcomeTitle')}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<p>
|
||||
{t('credentials.welcomeDescription')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -16,8 +16,8 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import HeaderButton from '../components/HeaderButton';
|
||||
import { HeaderIconType } from '../components/Icons/HeaderIcons';
|
||||
import HeaderButton from '../../components/HeaderButton';
|
||||
import { HeaderIconType } from '../../components/Icons/HeaderIcons';
|
||||
|
||||
/**
|
||||
* Email details page.
|
||||
@@ -32,6 +32,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
const [email, setEmail] = useState<Email | null>(null);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showMetadata, setShowMetadata] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [headerButtonsConfigured, setHeaderButtonsConfigured] = useState(false);
|
||||
@@ -207,21 +208,44 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>{t('emails.from')} {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
<p>{t('emails.to')} {email.toLocal}@{email.toDomain}</p>
|
||||
<p>{t('emails.date')} {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
<div>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
<button
|
||||
onClick={() => setShowMetadata(!showMetadata)}
|
||||
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title={showMetadata ? t('common.hideDetails') : t('common.showDetails')}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${showMetadata ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showMetadata && (
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p><span className="font-bold">{t('emails.from')}</span> <span title={email.fromLocal + "@" + email.fromDomain}>{email.fromDisplay}</span></p>
|
||||
<p><span className="font-bold">{t('emails.to')}</span> <span title={email.toLocal + "@" + email.toDomain}>{email.toLocal}@{email.toDomain}</span></p>
|
||||
<p><span className="font-bold">{t('emails.date')}</span> {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Body */}
|
||||
<div className="bg-white">
|
||||
<div className="bg-white mt-4">
|
||||
{email.messageHtml ? (
|
||||
<iframe
|
||||
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
|
||||
@@ -177,14 +177,14 @@ const EmailsList: React.FC = () => {
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="text-sm text-gray-900 dark:text-white mb-1 font-bold">
|
||||
<div className="text-gray-900 dark:text-white mb-1 font-bold">
|
||||
{email.subject}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatEmailDate(email.dateSystem)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
||||
<div className="text-gray-600 text-sm dark:text-gray-300 line-clamp-2">
|
||||
{email.messagePreview}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -48,14 +48,14 @@ const AutoLockSettings: React.FC = () => {
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
|
||||
<HelpModal
|
||||
titleKey="settings.autoLockTimeout"
|
||||
contentKey="settings.autoLockTimeoutHelp"
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.autoLockTimeoutDescription')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autoLockTimeoutDescription')}</p>
|
||||
<select
|
||||
value={autoLockTimeout}
|
||||
onChange={(e) => setAutoLockTimeoutSetting(Number(e.target.value))}
|
||||
|
||||
@@ -167,8 +167,8 @@ const AutofillSettings: React.FC = () => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
|
||||
<p className={`text-xs mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -195,12 +195,12 @@ const AutofillSettings: React.FC = () => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
|
||||
<p className={`text-xs mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
|
||||
</p>
|
||||
{!settings.isEnabled && settings.temporaryDisabledUrls[settings.currentUrl] && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('settings.temporarilyDisabledUntil')}{new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
@@ -238,8 +238,8 @@ const AutofillSettings: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.autofillMatchingMode')}</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.autofillMatchingModeDescription')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.autofillMatchingMode')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autofillMatchingModeDescription')}</p>
|
||||
<select
|
||||
value={autofillMatchingMode}
|
||||
onChange={(e) => setAutofillMatchingModeSetting(e.target.value as AutofillMatchingMode)}
|
||||
|
||||
@@ -46,8 +46,8 @@ const ClipboardSettings: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.clipboardClearTimeout')}</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.clipboardClearTimeoutDescription')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.clipboardClearTimeout')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.clipboardClearTimeoutDescription')}</p>
|
||||
<select
|
||||
value={clipboardTimeout}
|
||||
onChange={(e) => setClipboardClearTimeout(Number(e.target.value))}
|
||||
|
||||
@@ -49,11 +49,11 @@ const ContextMenuSettings: React.FC = () => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
|
||||
<p className={`text-xs mt-1 ${isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
|
||||
<p className={`text-sm mt-1 ${isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{isContextMenuEnabled ? t('settings.contextMenuEnabled') : t('settings.contextMenuDisabled')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
{t('settings.contextMenuDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
/**
|
||||
* Language settings page component.
|
||||
*/
|
||||
const LanguageSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
// Mark initial loading as complete
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -16,7 +23,7 @@ const LanguageSettings: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-3">{t('settings.selectLanguage')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-3">{t('settings.selectLanguage')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,10 +167,10 @@ const Settings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<p className="text font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@ const Settings: React.FC = () => {
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
@@ -256,7 +256,7 @@ const Settings: React.FC = () => {
|
||||
d="M4 6h16M4 12h16m-7 6h7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
@@ -288,7 +288,7 @@ const Settings: React.FC = () => {
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
@@ -320,7 +320,7 @@ const Settings: React.FC = () => {
|
||||
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
@@ -352,7 +352,7 @@ const Settings: React.FC = () => {
|
||||
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm text-gray-900 dark:text-white">{t('settings.language')}</span>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.language')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
@@ -374,7 +374,7 @@ const Settings: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -385,7 +385,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -396,7 +396,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -407,7 +407,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,7 +423,7 @@ const Settings: React.FC = () => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
@@ -1,3 +1,7 @@
|
||||
body {
|
||||
font-size: 75%;
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
@@ -1,392 +1,392 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginTitle": "Se connecter à AliasVault",
|
||||
"username": "Nom d'utilisateur ou email",
|
||||
"usernamePlaceholder": "nom / nom@entreprise.com",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Saisissez votre mot de passe",
|
||||
"rememberMe": "Se souvenir de moi",
|
||||
"loginButton": "Se connecter",
|
||||
"noAccount": "Pas de compte?",
|
||||
"createVault": "Créer un nouveau coffre",
|
||||
"twoFactorTitle": "Veuillez entrer le code d'authentification de votre application d'authentification.",
|
||||
"authCode": "Code d'authentification",
|
||||
"authCodePlaceholder": "Saisissez le code à 6 chiffres",
|
||||
"verify": "Vérifier",
|
||||
"cancel": "Annuler",
|
||||
"twoFactorNote": "Remarque : si vous n'avez pas accès à votre appareil d'authentification, vous pouvez réinitialiser votre authentification à double facteur avec un code de récupération en vous connectant via le site web.",
|
||||
"masterPassword": "Mot de passe principal",
|
||||
"unlockVault": "Déverrouiller le coffre",
|
||||
"unlockTitle": "Déverrouiller votre coffre",
|
||||
"unlockDescription": "Entrez votre mot de passe principal pour déverrouiller votre coffre-fort.",
|
||||
"logout": "Se déconnecter",
|
||||
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter.",
|
||||
"unlockSuccess": "Parcourir le contenu du coffre",
|
||||
"unlockSuccessTitle": "Votre coffre a été déverrouillé avec succès",
|
||||
"unlockSuccessDescription": "Vous pouvez maintenant utiliser le remplissage automatique des formulaires de connexion dans votre navigateur.",
|
||||
"closePopup": "Fermer cette popup",
|
||||
"browseVault": "Parcourir le contenu du coffre",
|
||||
"connectingTo": "Connexion à",
|
||||
"switchAccounts": "Changer de compte ?",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
"invalidCode": "Veuillez entrer un code d'authentification valide à 6 chiffres.",
|
||||
"serverError": "Impossible d'accéder au serveur AliasVault. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
|
||||
"noToken": "Échec de la connexion -- aucun jeton retourné",
|
||||
"migrationError": "Une erreur s'est produite lors de la vérification des migrations en attente.",
|
||||
"wrongPassword": "Mot de passe incorrect, veuillez réessayer.",
|
||||
"accountLocked": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives échouées.",
|
||||
"networkError": "Erreur réseau. Vérifiez votre connexion et réessayez.",
|
||||
"loginDataMissing": "La session a expiré. Veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"credentials": "Identifiants",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
"settings": "Réglages"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"loading": "Chargement...",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"cancel": "Annuler",
|
||||
"use": "Utiliser",
|
||||
"delete": "Supprimer",
|
||||
"close": "Fermer",
|
||||
"copied": "Copié !",
|
||||
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Cacher le mot de passe",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"loadingEmails": "Chargement des emails...",
|
||||
"loadingTotpCodes": "Chargement des codes TOTP...",
|
||||
"attachments": "Pièces jointes",
|
||||
"loadingAttachments": "Chargement des pièces jointes...",
|
||||
"settings": "Réglages",
|
||||
"recentEmails": "Emails récents",
|
||||
"loginCredentials": "Identifiants de connexion",
|
||||
"twoFactorAuthentication": "Authentification à double facteur",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"fullName": "Nom complet",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"birthDate": "Date de naissance",
|
||||
"nickname": "Surnom",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"syncingVault": "Synchronisation du coffre",
|
||||
"savingChangesToVault": "Enregistrement des modifications dans le coffre",
|
||||
"uploadingVaultToServer": "Envoi du coffre vers le serveur",
|
||||
"checkingVaultUpdates": "Vérification des mises à jour du coffre",
|
||||
"syncingUpdatedVault": "Synchronisation du coffre mis à jour",
|
||||
"executingOperation": "Exécution de l'opération...",
|
||||
"loadMore": "Voir plus",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
"VaultOutdated": "Votre coffre est obsolète. Veuillez vous connecter sur le site AliasVault et suivre les étapes.",
|
||||
"serverNotAvailable": "Le serveur d'AliasVault n'est pas disponible. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
|
||||
"clientVersionNotSupported": "Cette version de l'extension de navigateur AliasVault n'est plus prise en charge par le serveur. Veuillez mettre à jour votre extension de navigateur à la dernière version.",
|
||||
"serverVersionNotSupported": "Le serveur d'AliasVault doit être mis à jour vers une version plus récente afin d'utiliser cette extension de navigateur. Veuillez contacter le support si vous avez besoin d'aide.",
|
||||
"unknownError": "Une erreur inconnue s'est produite",
|
||||
"failedToStoreVault": "Échec du stockage du coffre",
|
||||
"vaultNotAvailable": "Coffre non disponible",
|
||||
"failedToRetrieveData": "Échec de la récupération des données",
|
||||
"vaultIsLocked": "Le coffre est verrouillé",
|
||||
"failedToUploadVault": "Échec du téléchargement du coffre",
|
||||
"passwordChanged": "Votre mot de passe a changé depuis la dernière fois que vous vous êtes connecté. Veuillez vous reconnecter pour des raisons de sécurité."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite. Merci de réessayer.",
|
||||
"ACCOUNT_LOCKED": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives infructueuses. Veuillez réessayer plus tard.",
|
||||
"ACCOUNT_BLOCKED": "Votre compte a été désactivé. Si vous pensez que c'est une erreur, veuillez contacter le support.",
|
||||
"USER_NOT_FOUND": "Nom d'utilisateur ou mot de passe invalide. Veuillez réessayer.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Code d'authentification invalide. Veuillez réessayer.",
|
||||
"INVALID_RECOVERY_CODE": "Code de récupération invalide. Veuillez réessayer.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Un jeton d'actualisation est requis.",
|
||||
"INVALID_REFRESH_TOKEN": "Jeton d'actualisation invalide.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Le jeton d'actualisation a été révoqué.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "L'enregistrement d'un nouveau compte est actuellement désactivé sur ce serveur. Veuillez contacter l'administrateur.",
|
||||
"USERNAME_REQUIRED": "Nom d’utilisateur requis.",
|
||||
"USERNAME_ALREADY_IN_USE": "Nom d'utilisateur déjà utilisé.",
|
||||
"USERNAME_AVAILABLE": "Ce nom d'utilisateur est disponible.",
|
||||
"USERNAME_MISMATCH": "Le nom d'utilisateur ne correspond pas à l'utilisateur actuel.",
|
||||
"PASSWORD_MISMATCH": "Le mot de passe indiqué ne correspond pas à votre mot de passe actuel.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Compte supprimé avec succès.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Le nom d'utilisateur ne peut pas être vide ou contenir un espace.",
|
||||
"USERNAME_TOO_SHORT": "Le nom d'utilisateur est trop court : il doit comporter au moins 3 caractères.",
|
||||
"USERNAME_TOO_LONG": "Le nom d'utilisateur est trop long : il ne peut pas contenir plus de 40 caractères.",
|
||||
"USERNAME_INVALID_EMAIL": "Adresse e-mail invalide.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Le nom d'utilisateur n'est pas valide, il ne peut contenir que des lettres ou des chiffres.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Votre coffre n'est pas à jour. Veuillez synchroniser votre coffre et réessayer.",
|
||||
"INTERNAL_SERVER_ERROR": "Erreur interne du serveur.",
|
||||
"VAULT_ERROR": "Le coffre local n'est pas à jour. Veuillez synchroniser votre coffre en rafraîchissant la page et réessayez."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"or": "ou",
|
||||
"new": "Nouveautés",
|
||||
"cancel": "Annuler",
|
||||
"search": "Rechercher",
|
||||
"vaultLocked": "AliasVault est verrouillé.",
|
||||
"creatingNewAlias": "Création de nouveaux alias...",
|
||||
"noMatchesFound": "Aucun résultat trouvé",
|
||||
"searchVault": "Rechercher dans le coffre...",
|
||||
"serviceName": "Nom du service",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"enterServiceName": "Entrez le nom du service",
|
||||
"enterEmailAddress": "Entrer l'adresse email",
|
||||
"enterUsername": "Entrez le nom d'utilisateur",
|
||||
"hideFor1Hour": "Cacher pendant 1 heure (site actuel)",
|
||||
"hidePermanently": "Masquer définitivement (site actuel)",
|
||||
"createRandomAlias": "Créer un alias aléatoire",
|
||||
"createUsernamePassword": "Créer un nom d'utilisateur/mot de passe",
|
||||
"randomAlias": "Alias aléatoire",
|
||||
"usernamePassword": "Nom d’utilisateur / mot de passe",
|
||||
"createAndSaveAlias": "Créer et enregistrer l'alias",
|
||||
"createAndSaveCredential": "Créer et enregistrer les identifiants",
|
||||
"randomIdentityDescription": "Générer une identité aléatoire avec une adresse email aléatoire accessible dans AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Identité aléatoire avec email aléatoire",
|
||||
"manualCredentialDescription": "Spécifiez votre propre adresse email et nom d'utilisateur.",
|
||||
"manualCredentialDescriptionDropdown": "Identifiant et mot de passe manuels",
|
||||
"failedToCreateIdentity": "Échec de la création de l'identité. Veuillez réessayer.",
|
||||
"enterEmailAndOrUsername": "Entrez l'adresse email et/ou le nom d'utilisateur",
|
||||
"autofillWithAliasVault": "Remplissage automatique avec AliasVault",
|
||||
"generateRandomPassword": "Générer un mot de passe aléatoire (copier dans le presse-papier)",
|
||||
"generateNewPassword": "Générer un nouveau mot de passe",
|
||||
"togglePasswordVisibility": "Afficher ou masquer le mot de passe",
|
||||
"passwordCopiedToClipboard": "Mot de passe copié dans le presse-papiers",
|
||||
"enterEmailAndOrUsernameError": "Entrez l'adresse email et/ou le nom d'utilisateur",
|
||||
"openAliasVaultToUpgrade": "Ouvrez AliasVault pour améliorer",
|
||||
"vaultUpgradeRequired": "Mise à niveau du coffre requise.",
|
||||
"dismissPopup": "Fermer"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"title": "Identifiants",
|
||||
"addCredential": "Ajouter des identifiants",
|
||||
"editCredential": "Modifier les identifiants",
|
||||
"deleteCredential": "Supprimer les identifiants",
|
||||
"credentialDetails": "Informations sur les identifiants",
|
||||
"serviceName": "Nom du service",
|
||||
"serviceNamePlaceholder": "ex: Gmail, Facebook, Banque",
|
||||
"website": "Site Internet",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"username": "Nom d'utilisateur",
|
||||
"usernamePlaceholder": "Entrez le nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Saisir le mot de passe",
|
||||
"generatePassword": "Générer le mot de passe",
|
||||
"copyPassword": "Copier le mot de passe",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Masquer le mot de passe",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"notesPlaceholder": "Notes supplémentaires...",
|
||||
"totp": "Authentification à deux facteurs",
|
||||
"totpCode": "Mot de passe à usage unique",
|
||||
"copyTotp": "Copier le mot de passe à usage unique",
|
||||
"totpSecret": "Mot de passe à usage unique secret",
|
||||
"totpSecretPlaceholder": "Entrez le mot de passe à usage unique",
|
||||
"noCredentials": "Aucun identifiant trouvé",
|
||||
"noCredentialsDescription": "Ajoutez vos premiers identifiants pour commencer",
|
||||
"searchPlaceholder": "Rechercher des identifiants...",
|
||||
"welcomeTitle": "Bienvenue dans AliasVault !",
|
||||
"welcomeDescription": "Pour utiliser l'extension de navigateur AliasVault : accédez à un site web et utilisez la fenêtre de saisie automatique AliasVault pour créer un nouvel identifiant.",
|
||||
"createdAt": "Créé",
|
||||
"updatedAt": "Dernière mise à jour",
|
||||
"autofill": "Remplissage automatique",
|
||||
"fillForm": "Remplir le formulaire",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cet identifiant ?",
|
||||
"saveSuccess": "Identifiants enregistrés avec succès",
|
||||
"tags": "Mots-clés",
|
||||
"addTag": "Ajouter un mot-clé",
|
||||
"removeTag": "Supprimer un mot-clé",
|
||||
"folder": "Dossier",
|
||||
"selectFolder": "Sélectionner un dossier",
|
||||
"createFolder": "Nouveau dossier",
|
||||
"saveCredential": "Enregistrer les identifiants",
|
||||
"deleteCredentialTitle": "Supprimer les identifiants",
|
||||
"deleteCredentialConfirm": "Êtes-vous sûr de vouloir supprimer ces identifiants ? Cette action est irréversible.",
|
||||
"randomAlias": "Alias aléatoire",
|
||||
"manual": "Manuel",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"serviceUrl": "URL de service",
|
||||
"loginCredentials": "Identifiants de connexion",
|
||||
"generateRandomUsername": "Générer un nom d'utilisateur aléatoire",
|
||||
"generateRandomPassword": "Générer un mot de passe aléatoire",
|
||||
"changePasswordComplexity": "Changer la complexité du mot de passe",
|
||||
"passwordLength": "Longueur du mot de passe",
|
||||
"includeLowercase": "Inclure les lettres minuscules",
|
||||
"includeUppercase": "Inclure les lettres majuscules",
|
||||
"includeNumbers": "Inclure des chiffres",
|
||||
"includeSpecialChars": "Inclure des caractères spéciaux",
|
||||
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
|
||||
"generateNewPreview": "Générer un nouvel aperçu",
|
||||
"generateRandomAlias": "Créer un alias aléatoire",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"nickName": "Surnom",
|
||||
"gender": "Genre",
|
||||
"birthDate": "Date de naissance",
|
||||
"birthDatePlaceholder": "AAAA-MM-JJ",
|
||||
"metadata": "Métadonnées",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
"required": "Ce champ est obligatoire",
|
||||
"serviceNameRequired": "Le nom du service est requis",
|
||||
"invalidEmail": "Format de courriel non valide",
|
||||
"invalidDateFormat": "La date doit être au format AAAA-MM-JJ"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
"privateEmailTitle": "Email privé",
|
||||
"privateEmailAliasVaultServer": "Serveur AliasVault",
|
||||
"privateEmailDescription": "Chiffrement bout en bout, entièrement privé.",
|
||||
"publicEmailTitle": "Fournisseurs d'email public temporaires",
|
||||
"publicEmailDescription": "Anonyme mais confidentialité limitée. Le contenu de l'email est lisible par toute personne qui connaît l'adresse.",
|
||||
"useDomainChooser": "Utiliser le sélecteur de domaine",
|
||||
"enterCustomDomain": "Entrez le domaine personnalisé",
|
||||
"enterFullEmail": "Entrez l'adresse email complète",
|
||||
"enterEmailPrefix": "Entrez le préfixe de l'email"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"deleteEmailTitle": "Supprimer l'email",
|
||||
"deleteEmailConfirm": "Êtes-vous sûr de vouloir supprimer définitivement cet email ?",
|
||||
"from": "De",
|
||||
"to": "À",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"emailContent": "Contenu de l'email",
|
||||
"attachments": "Pièces jointes",
|
||||
"emailNotFound": "Email introuvable",
|
||||
"noEmails": "Aucun email trouvé",
|
||||
"noEmailsDescription": "Vous n'avez pas encore reçu d'emails dans vos adresses email privées. Quand vous recevez un nouvel email, il apparaîtra ici.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
"justNow": "maintenant",
|
||||
"minutesAgo_single": "Il y a {{count}} minute",
|
||||
"minutesAgo_plural": "Il y a {{count}} minutes",
|
||||
"hoursAgo_single": "Il y a {{count}} heure",
|
||||
"hoursAgo_plural": "Il y a {{count}} heures",
|
||||
"yesterday": "hier"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
"emailLoadError": "Une erreur s'est produite lors du chargement des emails. Veuillez réessayer plus tard.",
|
||||
"emailUnexpectedError": "Une erreur inattendue s'est produite lors du chargement des emails. Veuillez réessayer plus tard."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "L'adresse email actuelle est déjà utilisée. Veuillez modifier l'adresse email en modifiant cet identifiant.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Une erreur s'est produite en essayant de charger les emails. Veuillez essayer de modifier et enregistrer les informations d'identification pour synchroniser la base de données, puis réessayez."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"title": "Réglages",
|
||||
"serverUrl": "URL du serveur",
|
||||
"language": "Langue",
|
||||
"autofillEnabled": "Activer le remplissage automatique",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
|
||||
"openWebApp": "Ouvrir l’application web",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"logout": "Se déconnecter",
|
||||
"globalSettings": "Paramètres généraux",
|
||||
"autofillPopup": "Remplissage automatique de la popup",
|
||||
"activeOnAllSites": "Activé sur tous les sites (sauf si désactivé ci-dessous)",
|
||||
"disabledOnAllSites": "Désactivé sur tous les sites",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"rightClickContextMenu": "Clic-droit sur le menu contextuel",
|
||||
"autofillMatching": "Correspondance de remplissage automatique",
|
||||
"autofillMatchingMode": "Remplir automatiquement le mode correspondant",
|
||||
"autofillMatchingModeDescription": "Détermine quels identifiants sont considérés comme une correspondance et sont affichés comme des suggestions dans la fenêtre de saisie automatique pour un site web donné.",
|
||||
"autofillMatchingDefault": "URL + sous-domaine + nom générique",
|
||||
"autofillMatchingUrlSubdomain": "URL + sous-domaine",
|
||||
"autofillMatchingUrlExact": "Domaine d'URL exact uniquement",
|
||||
"siteSpecificSettings": "Paramètres spécifiques au site",
|
||||
"autofillPopupOn": "Popup de saisie automatique sur: ",
|
||||
"enabledForThisSite": "Activé pour ce site",
|
||||
"disabledForThisSite": "Désactivé pour ce site",
|
||||
"temporarilyDisabledUntil": "Temporairement désactivé jusqu'au ",
|
||||
"resetAllSiteSettings": "Réinitialiser tous les paramètres spécifiques au site",
|
||||
"appearance": "Apparence",
|
||||
"theme": "Thème",
|
||||
"useDefault": "Utiliser par défaut",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"configureKeyboardShortcuts": "Configurer les raccourcis clavier",
|
||||
"configure": "Configurer",
|
||||
"security": "Sécurité",
|
||||
"clipboardClearTimeout": "Effacer le presse-papiers après copie",
|
||||
"clipboardClearTimeoutDescription": "Effacer automatiquement le presse-papiers après copie des données sensibles",
|
||||
"clipboardClearDisabled": "Ne jamais effacer",
|
||||
"clipboardClear5Seconds": "Effacer après 5 secondes",
|
||||
"clipboardClear10Seconds": "Effacer après 10 secondes",
|
||||
"clipboardClear15Seconds": "Effacer après 15 secondes",
|
||||
"autoLockTimeout": "Délai de verrouillage automatique",
|
||||
"autoLockTimeoutDescription": "Verrouiller automatiquement le coffre après une période d'inactivité",
|
||||
"autoLockTimeoutHelp": "Le coffre ne se verrouille qu'après la période d'inactivité spécifiée (aucune fenêtre pop-up de saisie automatique ou d'extension). Le coffre sera toujours verrouillé lorsque le navigateur sera fermé, quel que soit ce paramètre.",
|
||||
"autoLockNever": "Jamais",
|
||||
"autoLock15Seconds": "15 secondes",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"autoLock1Hour": "1 heure",
|
||||
"autoLock4Hours": "4 heures",
|
||||
"autoLock8Hours": "8 heures",
|
||||
"autoLock24Hours": "24 heures",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"preferences": "Préférences",
|
||||
"autofillSettings": "Paramètres du remplissage automatique",
|
||||
"clipboardSettings": "Paramètres du presse-papiers",
|
||||
"contextMenuSettings": "Paramètres du menu contextuel",
|
||||
"contextMenu": "Menu contextuel",
|
||||
"contextMenuEnabled": "Le menu contextuel est activé",
|
||||
"contextMenuDisabled": "Le menu contextuel est désactivé",
|
||||
"contextMenuDescription": "Faites un clic droit sur les champs de saisie pour accéder aux options d'AliasVault",
|
||||
"selectLanguage": "Sélectionner une langue",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
"apiUrlRequired": "L'URL de l'API est requise",
|
||||
"apiUrlInvalid": "Veuillez entrer une URL d'API valide",
|
||||
"clientUrlRequired": "L'URL du client est requise",
|
||||
"clientUrlInvalid": "Veuillez entrer une URL de client valide"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"title": "Mettre à niveau le coffre",
|
||||
"subtitle": "AliasVault a mis à jour et votre coffre doit être mis à niveau. Cela ne devrait prendre que quelques secondes.",
|
||||
"versionInformation": "Informations de version",
|
||||
"yourVault": "Votre coffre :",
|
||||
"newVersion": "Nouvelle version :",
|
||||
"upgrade": "Mettre le coffre à niveau",
|
||||
"upgrading": "Mise à niveau...",
|
||||
"logout": "Se déconnecter",
|
||||
"whatsNew": "Nouveautés",
|
||||
"whatsNewDescription": "Une mise à niveau est nécessaire pour prendre en charge les modifications suivantes :",
|
||||
"noDescriptionAvailable": "Aucune description disponible pour cette version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
"preparingUpgrade": "Préparation de la mise à niveau...",
|
||||
"vaultAlreadyUpToDate": "Le coffre est déjà à jour",
|
||||
"startingDatabaseTransaction": "Démarrage de la transaction de la base de données...",
|
||||
"applyingDatabaseMigrations": "Application des migrations de base de données...",
|
||||
"applyingMigration": "Application de la migration {{current}} sur {{total}}...",
|
||||
"committingChanges": "Validation des modifications..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
"error": "Erreur",
|
||||
"unableToGetVersionInfo": "Impossible d'obtenir les informations de version. Veuillez réessayer.",
|
||||
"selfHostedServer": "Serveur auto-hébergé",
|
||||
"selfHostedWarning": "Si vous utilisez un serveur auto-hébergé, assurez-vous également de mettre à jour votre instance auto-hébergée, sinon la connexion au client web cessera de fonctionner.",
|
||||
"cancel": "Annuler",
|
||||
"continueUpgrade": "Continuer la mise à jour",
|
||||
"upgradeFailed": "Échec de la mise à niveau",
|
||||
"failedToApplyMigration": "Impossible d'appliquer la migration ({{current}} sur {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Une erreur inconnue s'est produite pendant la mise à niveau. Veuillez réessayer."
|
||||
}
|
||||
}
|
||||
}
|
||||
392
apps/browser-extension/src/i18n/locales/he.json
Normal file
392
apps/browser-extension/src/i18n/locales/he.json
Normal file
@@ -0,0 +1,392 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "כניסה ל־AliasVault",
|
||||
"username": "שם משתמש או דוא״ל",
|
||||
"usernamePlaceholder": "שם / name@company.com",
|
||||
"password": "סיסמה",
|
||||
"passwordPlaceholder": "נא למלא את הסיסמה שלך",
|
||||
"rememberMe": "לזכור אותי",
|
||||
"loginButton": "כניסה",
|
||||
"noAccount": "אין לך חשבון עדיין?",
|
||||
"createVault": "יצירת כספת חדשה",
|
||||
"twoFactorTitle": "נא למלא את קוד האימות מיישומון המאמת שלך.",
|
||||
"authCode": "קוד אימות",
|
||||
"authCodePlaceholder": "נא למלא קוד באורך 6 ספרות",
|
||||
"verify": "אימות",
|
||||
"cancel": "ביטול",
|
||||
"twoFactorNote": "לתשומת ליבך: אם אין לך גישה להתקן המאמת (authenticator) שלך, אפשר לאפס אימות דו־שלבי עם קוד שחזור על ידי כניסה דרך האתר.",
|
||||
"masterPassword": "סיסמת על",
|
||||
"unlockVault": "שחרור נעילת כספת",
|
||||
"unlockTitle": "שחרור נעילת הכספת שלך",
|
||||
"unlockDescription": "נא למלא את סיסמת העל שלך כדי לשחרר את הכספת שלך.",
|
||||
"logout": "יציאה",
|
||||
"logoutConfirm": "לצאת?",
|
||||
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש.",
|
||||
"unlockSuccess": "נעילת הכספת שוחררה בהצלחה!",
|
||||
"unlockSuccessTitle": "נעילת הכספת שלך נפתחה בהצלחה",
|
||||
"unlockSuccessDescription": "מעתה ניתן להשתמש בהשלמה אוטומטית בטופסי כניסה בדפדפן שלך.",
|
||||
"closePopup": "סגירת החלונית הצצה הזאת",
|
||||
"browseVault": "עיון בתוכן הכספת",
|
||||
"connectingTo": "מתבצעת התחברות אל",
|
||||
"switchAccounts": "להחליף חשבונות?",
|
||||
"loggedIn": "נכנסת",
|
||||
"errors": {
|
||||
"invalidCode": "נא למלא קוד אימות באורך 6 ספרות.",
|
||||
"serverError": "לא ניתן ליצור קשר עם השרת של AliasVault. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
|
||||
"noToken": "הכניסה נכשלה - לא הוחזר אסימון",
|
||||
"migrationError": "אירעה שגיאה בעת בדיקה לאיתור הסבות ממתינות.",
|
||||
"wrongPassword": "סיסמה שגויה. נא לנסות שוב.",
|
||||
"accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.",
|
||||
"networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.",
|
||||
"loginDataMissing": "תוקף ההפעלה שלך פג. נא לנסות שוב."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "פרטי גישה",
|
||||
"emails": "הודעות דוא״ל",
|
||||
"settings": "הגדרות"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "בטעינה…",
|
||||
"error": "שגיאה",
|
||||
"success": "הצליח",
|
||||
"cancel": "ביטול",
|
||||
"use": "להשתמש",
|
||||
"delete": "מחיקה",
|
||||
"close": "סגירה",
|
||||
"copied": "הועתק!",
|
||||
"openInNewWindow": "פתיחה בחלון חדש",
|
||||
"language": "שפה",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"showPassword": "הצגת סיסמה",
|
||||
"hidePassword": "הסתרת סיסמה",
|
||||
"copyToClipboard": "העתקה ללוח הגזירים",
|
||||
"loadingEmails": "הודעות הדוא״ל נטענות…",
|
||||
"loadingTotpCodes": "הקודים החד־פעמיים הזמניים נטענים…",
|
||||
"attachments": "צרופות",
|
||||
"loadingAttachments": "הצרופות נטענות…",
|
||||
"settings": "הגדרות",
|
||||
"recentEmails": "הודעות דוא״ל אחרונות",
|
||||
"loginCredentials": "פרטי הגישה",
|
||||
"twoFactorAuthentication": "אימות דו־שלבי",
|
||||
"alias": "כינוי",
|
||||
"notes": "הערות",
|
||||
"fullName": "שם מלא",
|
||||
"firstName": "שם פרטי",
|
||||
"lastName": "שם משפחה",
|
||||
"birthDate": "תאריך לידה",
|
||||
"nickname": "כינוי",
|
||||
"email": "דוא״ל",
|
||||
"username": "שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"syncingVault": "הכספת מסתנכרת",
|
||||
"savingChangesToVault": "השינוים לכספת נשמרים",
|
||||
"uploadingVaultToServer": "הכספת נשלחת לשרת",
|
||||
"checkingVaultUpdates": "מתבצעת בדיקה לשינויים בכספת",
|
||||
"syncingUpdatedVault": "הכספת העדכנית מסתנכרת",
|
||||
"executingOperation": "הפעולה רצה…",
|
||||
"loadMore": "לטעון עוד",
|
||||
"errors": {
|
||||
"VaultOutdated": "הכספת שלך לא עדכנית. נא להיכנס לאתר AliasVault ולעקוב אחר ההנחיות.",
|
||||
"serverNotAvailable": "שרת ה־AliasVault לא זמין. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
|
||||
"clientVersionNotSupported": "הגרסה הזאת של הרחבת הדפדפן של AliasVault לא נתמכת עוד על ידי השרת. נא לעדכן את הרחבת הדפדפן שלך לגרסה העדכנית ביותר.",
|
||||
"serverVersionNotSupported": "יש לעדכן את שרת AliasVault לגרסה חדשה יותר כדי להשתמש בהרחבת הדפדפן הזאת. נא ליצור קשר עם התמיכה לקבלת עזרה.",
|
||||
"unknownError": "אירעה שגיאה לא ידועה",
|
||||
"failedToStoreVault": "אחסון הכספת נכשל",
|
||||
"vaultNotAvailable": "הכספת לא זמינה",
|
||||
"failedToRetrieveData": "משיכת הנתונים נכשלה",
|
||||
"vaultIsLocked": "הכספת נעולה",
|
||||
"failedToUploadVault": "העלאת הכספת נכשלה",
|
||||
"passwordChanged": "הסיסמה שלך השתנתה מאז הפעם האחרונה שנכנסת למערכת. נא להיכנס שוב מטעמי אבטחת מידע."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "אירעה שגיאה לא ידועה, נא לנסות שוב.",
|
||||
"ACCOUNT_LOCKED": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים. נא לנסות שוב מאוחר יותר.",
|
||||
"ACCOUNT_BLOCKED": "החשבון שלך הושבת. אם לדעתך מדובר בטעות, נא ליצור קשר עם התמיכה.",
|
||||
"USER_NOT_FOUND": "שם המשתמש או הסיסמה שגויים. נא לנסות שוב.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "קוד מאמת שגוי. נא לנסות שוב.",
|
||||
"INVALID_RECOVERY_CODE": "קוד שחזור שגוי. נא לנסות שוב.",
|
||||
"REFRESH_TOKEN_REQUIRED": "אסימון ריענון חובה.",
|
||||
"INVALID_REFRESH_TOKEN": "אסימון ריענון שגוי.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "אסימון הריענון נשלל בהצלחה.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "רישום חשבון חדש מושבת כרגע בשרת הזה. נא ליצור קשר עם ההנהלה.",
|
||||
"USERNAME_REQUIRED": "שם משתמש חובה.",
|
||||
"USERNAME_ALREADY_IN_USE": "שם המשתמש כבר תפוס.",
|
||||
"USERNAME_AVAILABLE": "שם המשתמש פנוי.",
|
||||
"USERNAME_MISMATCH": "שם המשתמש לא מתאים למשתמש הנוכחי.",
|
||||
"PASSWORD_MISMATCH": "הסיסמה שסופקה לא תואמת לסיסמה הנוכחית שלך.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "החשבון נמחק בהצלחה.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "שם המשתמש לא יכול להיות ריק או מורכב מרווחים בלבד.",
|
||||
"USERNAME_TOO_SHORT": "שם המשתמש קצר מדי: חייב להיות באורך 3 תווים לפחות.",
|
||||
"USERNAME_TOO_LONG": "שם המשתמש ארוך מדי: לא יכול להיות ארוך מ־40 תווים.",
|
||||
"USERNAME_INVALID_EMAIL": "כתובת דוא״ל שגויה.",
|
||||
"USERNAME_INVALID_CHARACTERS": "שם המשתמש שגוי, יכול להכיל רק תווים או ספרות.",
|
||||
"VAULT_NOT_UP_TO_DATE": "הכספת שלך אינה עדכנית. נא לסנכרן את הכספת שלך ולנסות שוב.",
|
||||
"INTERNAL_SERVER_ERROR": "שגיאת שרת פנימית.",
|
||||
"VAULT_ERROR": "הכספת המקומית אינה עדכנית. נא לסנכרן את הכספת שלך על ידי ריענון העמוד ולנסות שוב."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "או",
|
||||
"new": "חדש",
|
||||
"cancel": "ביטול",
|
||||
"search": "חיפוש",
|
||||
"vaultLocked": "AliasVault נעול.",
|
||||
"creatingNewAlias": "נוצר כינוי חדש...",
|
||||
"noMatchesFound": "לא נמצאו תוצאות",
|
||||
"searchVault": "חיפוש בכספת…",
|
||||
"serviceName": "שם השירות",
|
||||
"email": "דוא״ל",
|
||||
"username": "שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"enterServiceName": "נא למלא את שם השירות",
|
||||
"enterEmailAddress": "נא למלא כתובת דוא״ל",
|
||||
"enterUsername": "נא למלא שם משתמש",
|
||||
"hideFor1Hour": "הסתרה למשך שעה (האתר הנוכחי)",
|
||||
"hidePermanently": "הסתרה לצמיתות (האתר הנוכחי)",
|
||||
"createRandomAlias": "יצירת כינוי אקראי",
|
||||
"createUsernamePassword": "יצירת שם משתמש/סיסמה",
|
||||
"randomAlias": "כינוי אקראי",
|
||||
"usernamePassword": "שם משתמש/סיסמה",
|
||||
"createAndSaveAlias": "יצירה ושמירה של כינוי",
|
||||
"createAndSaveCredential": "יצירה ושמירה של פרטי גישה",
|
||||
"randomIdentityDescription": "יצירת זהות אקראית עם כתובת דוא״ל אקראית שנגישה דרך AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "זהות אקראיות עם דוא״ל אקראי",
|
||||
"manualCredentialDescription": "נא לציין כתובת דוא״ל ושם משתמש משלך.",
|
||||
"manualCredentialDescriptionDropdown": "שם משתמש וסיסמה ידניים",
|
||||
"failedToCreateIdentity": "יצירת הזהות נכשלה. נא לנסות שוב.",
|
||||
"enterEmailAndOrUsername": "נא למלא דוא״ל ו/או שם משתמש",
|
||||
"autofillWithAliasVault": "השלמה אוטומטית עם AliasVault",
|
||||
"generateRandomPassword": "יצירת סיסמה אקראית (העתקה ללוח הגזירים)",
|
||||
"generateNewPassword": "יצירת סיסמה חדשה",
|
||||
"togglePasswordVisibility": "הצגת/הסתרת סיסמה",
|
||||
"passwordCopiedToClipboard": "הסיסמה הועתקה ללוח הגזירים",
|
||||
"enterEmailAndOrUsernameError": "נא למלא דוא״ל ו/או שם משתמש",
|
||||
"openAliasVaultToUpgrade": "יש לפתוח את AliasVault כדי לשדרג",
|
||||
"vaultUpgradeRequired": "יש לשדרג את הכספת.",
|
||||
"dismissPopup": "התעלמות מחלונית"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "פרטי גישה",
|
||||
"addCredential": "הוספת פרטי גישה",
|
||||
"editCredential": "עריכת פרטי גישה",
|
||||
"deleteCredential": "מחיקת פרטי גישה",
|
||||
"credentialDetails": "פירוט פרטי גישה",
|
||||
"serviceName": "שם השירות",
|
||||
"serviceNamePlaceholder": "למשל: ג׳ימייל, פייסבוק, בנק",
|
||||
"website": "אתר",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "שם משתמש",
|
||||
"usernamePlaceholder": "נא למלא שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"passwordPlaceholder": "נא למלא סיסמה",
|
||||
"generatePassword": "יצירת סיסמה",
|
||||
"copyPassword": "העתקת סיסמה",
|
||||
"showPassword": "הצגת סיסמה",
|
||||
"hidePassword": "הסתרת סיסמה",
|
||||
"notes": "הערות",
|
||||
"notesPlaceholder": "הערות נוספות…",
|
||||
"totp": "אימות דו־שלבי",
|
||||
"totpCode": "קוד חד־פעמי זמני",
|
||||
"copyTotp": "העתקת קוד חד־פעמי זמני",
|
||||
"totpSecret": "סוג סיסמה חד־פעמית זמנית",
|
||||
"totpSecretPlaceholder": "נא למלא מפתח סודי לסיסמה חד־פעמית זמנית",
|
||||
"noCredentials": "לא נמצאו פרטי גישה",
|
||||
"noCredentialsDescription": "יש להוסיף את פרטי הגישה הראשונים שלך כדי להתחיל",
|
||||
"searchPlaceholder": "חיפוש פרטי גישה…",
|
||||
"welcomeTitle": "ברוך בואך ל־AliasVault!",
|
||||
"welcomeDescription": "כדי להשתמש בהרחבת הדפדפן של AliasVault: יש לנווט לאתר ולהשתמש בחלונית ההשלמה האוטומטית של AliasVault כדי ליצור פרטי גישה חדשים.",
|
||||
"createdAt": "יצירה",
|
||||
"updatedAt": "עדכון אחרון",
|
||||
"autofill": "השלמה אוטומטית",
|
||||
"fillForm": "מילוי טופס",
|
||||
"deleteConfirm": "למחוק את פרטי הגישה האלה?",
|
||||
"saveSuccess": "פרטי הגישה נשמרו בהצלחה",
|
||||
"tags": "תגיות",
|
||||
"addTag": "הוספת תגית",
|
||||
"removeTag": "הסרת תגית",
|
||||
"folder": "תיקייה",
|
||||
"selectFolder": "בחירת תיקייה",
|
||||
"createFolder": "יצירת תיקייה",
|
||||
"saveCredential": "שמירת פרטי גישה",
|
||||
"deleteCredentialTitle": "מחיקת פרטי גישה",
|
||||
"deleteCredentialConfirm": "למחוק את פרטי הגישה? זאת פעולה בלתי הפיכה.",
|
||||
"randomAlias": "כינוי אקראי",
|
||||
"manual": "ידני",
|
||||
"service": "שירות",
|
||||
"serviceUrl": "כתובת השירות",
|
||||
"loginCredentials": "פרטי הגישה",
|
||||
"generateRandomUsername": "יצירת שם משתמש אקראי",
|
||||
"generateRandomPassword": "יצירת סיסמה אקראית",
|
||||
"changePasswordComplexity": "החלפת מורכבת הסיסמה",
|
||||
"passwordLength": "אורך הסיסמה",
|
||||
"includeLowercase": "לכלול אותיות קטנות",
|
||||
"includeUppercase": "לכלול אותיות גדולות",
|
||||
"includeNumbers": "לכלול מספרים",
|
||||
"includeSpecialChars": "לכלול תווים מיוחדים",
|
||||
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
|
||||
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
|
||||
"generateRandomAlias": "יצירת כינוי אקראי",
|
||||
"alias": "כינוי",
|
||||
"firstName": "שם פרטי",
|
||||
"lastName": "שם משפחה",
|
||||
"nickName": "כינוי",
|
||||
"gender": "מגדר",
|
||||
"birthDate": "תאריך לידה",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "נתוני על",
|
||||
"validation": {
|
||||
"required": "שדה חובה",
|
||||
"serviceNameRequired": "שם השירות חובה",
|
||||
"invalidEmail": "תבנית דוא״ל שגויה",
|
||||
"invalidDateFormat": "התאריך חייב להיות בתבנית YYYY-MM-DD"
|
||||
},
|
||||
"privateEmailTitle": "כתובת דוא״ל פרטית",
|
||||
"privateEmailAliasVaultServer": "שרת AliasVault",
|
||||
"privateEmailDescription": "הצפנה מקצה לקצה, פרטיות מלאה.",
|
||||
"publicEmailTitle": "ספקי תיבת דוא״ל זמנית ציבוריים",
|
||||
"publicEmailDescription": "פרטיות אלמונית אך מוגבלת. תוכן הדוא״ל נגיש לכל מי שיודע את הכתובת.",
|
||||
"useDomainChooser": "להשתמש בבורר שמות התחום",
|
||||
"enterCustomDomain": "נא למלא שם תחום מותאם אישית",
|
||||
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
|
||||
"enterEmailPrefix": "נא למלא קידומת דוא״ל"
|
||||
},
|
||||
"emails": {
|
||||
"title": "הודעות דוא״ל",
|
||||
"deleteEmailTitle": "מחיקת הודעת דוא״ל",
|
||||
"deleteEmailConfirm": "למחוק את הודעת הדוא״ל הזאת לצמיתות?",
|
||||
"from": "מאת",
|
||||
"to": "אל",
|
||||
"date": "תאריך",
|
||||
"emailContent": "תוכן הודעת דוא״ל",
|
||||
"attachments": "צרופות",
|
||||
"emailNotFound": "הודעת הדוא״ל לא נמצאה",
|
||||
"noEmails": "לא נמצאו הודעות דוא״ל",
|
||||
"noEmailsDescription": "לא קיבלת הודעות דוא״ל כלשהן לכתובות הדוא״ל הפרטיות שלך עדיין. כשמגיעה הודעה חדשה היא תופיע כאן.",
|
||||
"dateFormat": {
|
||||
"justNow": "ממש הרגע",
|
||||
"minutesAgo_single": "לפני דקה",
|
||||
"minutesAgo_plural": "לפני {{count}} דקות",
|
||||
"hoursAgo_single": "לפני שעה",
|
||||
"hoursAgo_plural": "לפני {{count}} שעות",
|
||||
"yesterday": "אתמול"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "אירעה שגיאה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר.",
|
||||
"emailUnexpectedError": "אירעה שגיאה לא צפויה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "כתובת הדוא״ל שנבחרה תפוסה. נא לשנות את כתובת הדוא״ל על ידי עריכת פרטי הגישה.",
|
||||
"CLAIM_DOES_NOT_EXIST": "אירעה שגיאה בניסיון לטעון את הודעות הדוא״ל. נא לנסות לערוך ולשמור את רשומת פרטי הקשר כדי לסנכרן את מסד הנתונים ואז לנסות שוב."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "הגדרות",
|
||||
"serverUrl": "כתובת שרת",
|
||||
"language": "שפה",
|
||||
"autofillEnabled": "הפעלת השלמה אוטומטית",
|
||||
"version": "גרסה",
|
||||
"openInNewWindow": "פתיחה בחלון חדש",
|
||||
"openWebApp": "פתיחת אתר",
|
||||
"loggedIn": "נכנסת",
|
||||
"logout": "יציאה",
|
||||
"globalSettings": "הגדרות מקיפות",
|
||||
"autofillPopup": "חלונית השלמה אוטומטית",
|
||||
"activeOnAllSites": "פעיל בכל האתרים (למעט אם נכבה להלן)",
|
||||
"disabledOnAllSites": "כבוי בכל האתרים",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"rightClickContextMenu": "תפריט הקשר בלחיצה ימנית",
|
||||
"autofillMatching": "התאמת השלמה אוטומטית",
|
||||
"autofillMatchingMode": "מצב התאמת השלמה אוטומטית",
|
||||
"autofillMatchingModeDescription": "הגדרה אילו פרטי גישה נחשבים תואמים ויופיעו כהצעות בחלונית ההשלמה האוטומטית לאתר מסוים.",
|
||||
"autofillMatchingDefault": "כתובת + שם תת־תחום + תו כל על שם",
|
||||
"autofillMatchingUrlSubdomain": "כתובת + שם תת־תחום",
|
||||
"autofillMatchingUrlExact": "תחום כתובת מדויקת בלבד",
|
||||
"siteSpecificSettings": "הגדרות תואמות אתר",
|
||||
"autofillPopupOn": "חלונית השלמה אוטומטית ב־: ",
|
||||
"enabledForThisSite": "פעיל לאתר הזה",
|
||||
"disabledForThisSite": "כבוי לאתר הזה",
|
||||
"temporarilyDisabledUntil": "כבוי זמנית עד ",
|
||||
"resetAllSiteSettings": "איפוס כל ההגדרות הנקודתיות לאתרים",
|
||||
"appearance": "מראה",
|
||||
"theme": "ערכת עיצוב",
|
||||
"useDefault": "להשתמש בברירת המחדל",
|
||||
"light": "בהירה",
|
||||
"dark": "כהה",
|
||||
"keyboardShortcuts": "קיצורי מקלדת",
|
||||
"configureKeyboardShortcuts": "הגדרת קיצורי מקלדת",
|
||||
"configure": "הגדרה",
|
||||
"security": "אבטחה",
|
||||
"clipboardClearTimeout": "לפנות את לוח הגזירים לאחר העתקה",
|
||||
"clipboardClearTimeoutDescription": "לפנות את לוח הגזירים אוטומטית לאחר העתקת נתונים רגישים",
|
||||
"clipboardClearDisabled": "אף פעם לא לפנות",
|
||||
"clipboardClear5Seconds": "לפנות אחרי 5 שניות",
|
||||
"clipboardClear10Seconds": "לפנות אחרי 10 שניות",
|
||||
"clipboardClear15Seconds": "לפנות אחרי 15 שניות",
|
||||
"autoLockTimeout": "תום המתנה לנעילה אוטומטית",
|
||||
"autoLockTimeoutDescription": "לנעול את הכספת אוטומטית לאחר פרק זמן של חוסר פעילות",
|
||||
"autoLockTimeoutHelp": "הכספת תינעל רק לאחר משך זמן של חוסר פעילות (אין שימוש בהשלמה אוטומטית או פתיחת חלונית הרחבה). הכספת תמיד תינעל עם סגירת הדפדפן, ללא תלות בהגדרה הזאת.",
|
||||
"autoLockNever": "אף פעם",
|
||||
"autoLock15Seconds": "15 שניות",
|
||||
"autoLock1Minute": "דקה",
|
||||
"autoLock5Minutes": "5 דקות",
|
||||
"autoLock15Minutes": "15 דקות",
|
||||
"autoLock30Minutes": "30 דקות",
|
||||
"autoLock1Hour": "שעה",
|
||||
"autoLock4Hours": "4 שעות",
|
||||
"autoLock8Hours": "8 שעות",
|
||||
"autoLock24Hours": "24 שעות",
|
||||
"versionPrefix": "גרסה ",
|
||||
"preferences": "העדפות",
|
||||
"autofillSettings": "הגדרות השלמה אוטומטית",
|
||||
"clipboardSettings": "הגדרות לוח הגזירים",
|
||||
"contextMenuSettings": "הגדרות תפריט הקשר",
|
||||
"contextMenu": "תפריט הקשר",
|
||||
"contextMenuEnabled": "תפריט הקשר פעיל",
|
||||
"contextMenuDisabled": "תפריט הקשר כבוי",
|
||||
"contextMenuDescription": "ניתן ללחוץ על שדה עם הלחצן הימני כדי לגשת לאפשרויות AliasVault",
|
||||
"selectLanguage": "בחירת שפה",
|
||||
"validation": {
|
||||
"apiUrlRequired": "כתובת API חובה",
|
||||
"apiUrlInvalid": "נא למלא כתובת API תקפה",
|
||||
"clientUrlRequired": "כתובת לקוח חובה",
|
||||
"clientUrlInvalid": "נא למלא כתובת לקוח תקפה"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "שדרוג כספת",
|
||||
"subtitle": "AliasVault התעדכן וצריך לשדרג את הכספת שלך. הפעולה הזאת אמורה לארוך מספר שניות.",
|
||||
"versionInformation": "פרטי גרסה",
|
||||
"yourVault": "הכספת שלך:",
|
||||
"newVersion": "גרסה חדשה:",
|
||||
"upgrade": "שדרוג כספת",
|
||||
"upgrading": "משתדרגת…",
|
||||
"logout": "יציאה",
|
||||
"whatsNew": "מה חדש",
|
||||
"whatsNewDescription": "יש לשדרג כדי שתהיה תמיכה בשינויים הבאים:",
|
||||
"noDescriptionAvailable": "אין תיאור זמין לגרסה הזאת.",
|
||||
"okay": "אישור",
|
||||
"status": {
|
||||
"preparingUpgrade": "השדרוג בהכנה…",
|
||||
"vaultAlreadyUpToDate": "הכספת כבר עדכנית",
|
||||
"startingDatabaseTransaction": "הסבת מסד הנתונים מתחילה…",
|
||||
"applyingDatabaseMigrations": "השינויים חלים על מסד הנתונים…",
|
||||
"applyingMigration": "חלה ההסבה {{current}} מתוך {{total}}…",
|
||||
"committingChanges": "השינויים מקובעים…"
|
||||
},
|
||||
"alerts": {
|
||||
"error": "שגיאה",
|
||||
"unableToGetVersionInfo": "לא ניתן לקבל את פרטי הגרסה. נא לנסות שוב מאוחר יותר.",
|
||||
"selfHostedServer": "שרת באירוח עצמי",
|
||||
"selfHostedWarning": "אם מדובר בשרת שמתארח עצמאית, נא לוודא שהעותק שמתארח אצלך גם כן מתעדכן כי אחרת הכניסה לאתר תפסיק לעבוד.",
|
||||
"cancel": "ביטול",
|
||||
"continueUpgrade": "להמשיך בשדרוג",
|
||||
"upgradeFailed": "השדרוג נכשל",
|
||||
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})",
|
||||
"unknownErrorDuringUpgrade": "אירעה שגיאה בלתי ידועה במהלך השדרוג. נא לנסות שוב."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,10 +96,10 @@
|
||||
"unknownError": "Произошла неизвестная ошибка",
|
||||
"failedToStoreVault": "Не удалось сохранить хранилище",
|
||||
"vaultNotAvailable": "Хранилище недоступно",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"failedToRetrieveData": "Не удалось получить данные",
|
||||
"vaultIsLocked": "Хранилище заблокировано",
|
||||
"failedToUploadVault": "Не удалось загрузить хранилище",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
"passwordChanged": "С момента вашего последнего входа ваш пароль изменился. Пожалуйста, войдите еще раз в целях безопасности."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Произошла неизвестная ошибка. Пожалуйста, попробуйте снова.",
|
||||
@@ -243,15 +243,15 @@
|
||||
"invalidEmail": "Неверный формат электронной почты",
|
||||
"invalidDateFormat": "Дата должна быть указана в формате ГГГГ-ММ-ДД"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
"privateEmailTitle": "Личная электронная почта",
|
||||
"privateEmailAliasVaultServer": "Сервер AliasVault",
|
||||
"privateEmailDescription": "Шифрование E2E, полностью приватный.",
|
||||
"publicEmailTitle": "Общедоступные временные поставщики электронной почты",
|
||||
"publicEmailDescription": "Анонимность, но ограниченная конфиденциальность. Содержимое письма может прочитать любой, кому известен адрес.",
|
||||
"useDomainChooser": "Использовать выбор домена",
|
||||
"enterCustomDomain": "Ввести пользовательский домен",
|
||||
"enterFullEmail": "Введите полный адрес электронной почты",
|
||||
"enterEmailPrefix": "Введите префикс электронной почты"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Письма",
|
||||
@@ -299,12 +299,12 @@
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"rightClickContextMenu": "Контекстное меню правым щелчком мыши",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"autofillMatching": "Соответствие автозаполнения",
|
||||
"autofillMatchingMode": "Режим сопоставления автозаполнения",
|
||||
"autofillMatchingModeDescription": "Определяет, какие учетные данные считаются соответствующими и отображаются в качестве предложений во всплывающем окне автозаполнения для данного веб-сайта.",
|
||||
"autofillMatchingDefault": "URL + поддомен + подстановочный знак в названии",
|
||||
"autofillMatchingUrlSubdomain": "URL + поддомен",
|
||||
"autofillMatchingUrlExact": "Только точный URL-адрес домена",
|
||||
"siteSpecificSettings": "Настройки для конкретного сайта",
|
||||
"autofillPopupOn": "Всплывающее окно автозаполнения: ",
|
||||
"enabledForThisSite": "Включено для этого сайта",
|
||||
@@ -319,36 +319,36 @@
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"configureKeyboardShortcuts": "Настройка горячих клавиш",
|
||||
"configure": "Настройка",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"security": "Безопасность",
|
||||
"clipboardClearTimeout": "Очистить буфер обмена после копирования",
|
||||
"clipboardClearTimeoutDescription": "Автоматическая очистка буфера обмена после копирования конфиденциальных данных",
|
||||
"clipboardClearDisabled": "Никогда не очищать",
|
||||
"clipboardClear5Seconds": "Очистка через 5 секунд",
|
||||
"clipboardClear10Seconds": "Очистка через 10 секунд",
|
||||
"clipboardClear15Seconds": "Очистка через 15 секунд",
|
||||
"autoLockTimeout": "Тайм-аут автоматической блокировки",
|
||||
"autoLockTimeoutDescription": "Автоматическая блокировка хранилища после некоторого периода бездействия",
|
||||
"autoLockTimeoutHelp": "Хранилище будет заблокировано только по истечении указанного периода бездействия (не будет использоваться функция автозаполнения или не откроется всплывающее окно с расширением). Хранилище всегда будет заблокировано при закрытии браузера, независимо от этого параметра.",
|
||||
"autoLockNever": "Никогда",
|
||||
"autoLock15Seconds": "15 секунд",
|
||||
"autoLock1Minute": "1 минута",
|
||||
"autoLock5Minutes": "5 минут",
|
||||
"autoLock15Minutes": "15 минут",
|
||||
"autoLock30Minutes": "30 минут",
|
||||
"autoLock1Hour": "1 час",
|
||||
"autoLock4Hours": "4 часа",
|
||||
"autoLock8Hours": "8 часов",
|
||||
"autoLock24Hours": "24 часов",
|
||||
"versionPrefix": "Версия ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"preferences": "Предпочтения",
|
||||
"autofillSettings": "Настройки автозаполнения",
|
||||
"clipboardSettings": "Настройки буфера обмена",
|
||||
"contextMenuSettings": "Настройки контекстного меню",
|
||||
"contextMenu": "Контекстное меню",
|
||||
"contextMenuEnabled": "Контекстное меню включено",
|
||||
"contextMenuDisabled": "Контекстное меню отключено",
|
||||
"contextMenuDescription": "Щелкните правой кнопкой мыши на полях ввода, чтобы получить доступ к параметрам AliasVault",
|
||||
"selectLanguage": "Выбрать язык",
|
||||
"validation": {
|
||||
"apiUrlRequired": "Требуется URL-адрес API",
|
||||
"apiUrlInvalid": "Пожалуйста, введите корректный URL-адрес API",
|
||||
|
||||
@@ -299,12 +299,12 @@
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"rightClickContextMenu": "Контекстне меню правою кнопкою миші",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"autofillMatching": "Автозаповнення відповідності",
|
||||
"autofillMatchingMode": "Режим автозаповнення відповідностей",
|
||||
"autofillMatchingModeDescription": "Визначає, які облікові дані вважаються відповідними та будуть показуватися як пропозиції у спливному вікні автозаповнення для певного вебсайту.",
|
||||
"autofillMatchingDefault": "URL-адреса + піддомен + універсальне ім'я",
|
||||
"autofillMatchingUrlSubdomain": "URL-адреса + піддомен",
|
||||
"autofillMatchingUrlExact": "Лише точний домен URL-адреси",
|
||||
"siteSpecificSettings": "Налаштування, специфічні для сайту",
|
||||
"autofillPopupOn": "Спливаюче вікно автозаповнення на: ",
|
||||
"enabledForThisSite": "Увімкнено для цього сайту",
|
||||
@@ -326,7 +326,7 @@
|
||||
"clipboardClear5Seconds": "Очистити після 5 секунд",
|
||||
"clipboardClear10Seconds": "Очистити після 10 секунд",
|
||||
"clipboardClear15Seconds": "Очистити після 15 секунд",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeout": "Тайм-аут автоматичного блокування",
|
||||
"autoLockTimeoutDescription": "Автоматично блокувати сховище після періоду бездіяльності",
|
||||
"autoLockTimeoutHelp": "Сховище буде заблоковано лише після зазначеного періоду бездіяльності (не використовується автозаповнення або не відкривається спливне вікно розширення). Сховище завжди блокуватиметься, коли браузер закривається, незалежно від цього налаштування.",
|
||||
"autoLockNever": "Ніколи",
|
||||
@@ -340,15 +340,15 @@
|
||||
"autoLock8Hours": "8 годин",
|
||||
"autoLock24Hours": "24 години",
|
||||
"versionPrefix": "Версія ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"preferences": "Налаштування",
|
||||
"autofillSettings": "Налаштування автозаповнення",
|
||||
"clipboardSettings": "Параметри буфера обміну",
|
||||
"contextMenuSettings": "Налаштування контекстного меню",
|
||||
"contextMenu": "Контекстне меню",
|
||||
"contextMenuEnabled": "Контекстне меню увімкнено",
|
||||
"contextMenuDisabled": "Контекстне меню вимкнено",
|
||||
"contextMenuDescription": "Натисніть правою кнопкою миші на поля введення, щоб отримати доступ до параметрів AliasVault",
|
||||
"selectLanguage": "Виберіть мову",
|
||||
"validation": {
|
||||
"apiUrlRequired": "URL-адреса API обов'язкова",
|
||||
"apiUrlInvalid": "Будь ласка, введіть дійсну URL-адресу API",
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.22.0';
|
||||
public static readonly VERSION = '0.23.1';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineConfig({
|
||||
return {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.22.0",
|
||||
version: "0.23.1",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
ignorePatterns: ["dist/**", "node_modules/**", "utils/dist/shared/**", "expo-env.d.ts", "*.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: ".",
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"react",
|
||||
"react-hooks",
|
||||
"react-native",
|
||||
"import",
|
||||
"jsdoc",
|
||||
],
|
||||
extends: [
|
||||
"expo",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:react-native/all",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
"react-native/react-native": true,
|
||||
},
|
||||
globals: {
|
||||
__DEV__: "readonly",
|
||||
chrome: "readonly",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"import/ignore": ["node_modules/react-native/index\\.js"],
|
||||
'react-native/components': {
|
||||
Text: ['ThemedText'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// TypeScript
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/typedef": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
ignoreRestSiblings: true,
|
||||
varsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
selector: "interface",
|
||||
format: ["PascalCase"],
|
||||
prefix: ["I"],
|
||||
},
|
||||
{
|
||||
selector: "class",
|
||||
format: ["PascalCase"],
|
||||
},
|
||||
],
|
||||
|
||||
// React
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/no-unused-prop-types": "error",
|
||||
"react/jsx-no-constructed-context-values": "error",
|
||||
|
||||
// React Hooks
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
// React Native
|
||||
"react-native/no-unused-styles": "warn",
|
||||
"react-native/split-platform-components": "warn",
|
||||
"react-native/no-inline-styles": "warn",
|
||||
"react-native/no-color-literals": "warn",
|
||||
"react-native/no-single-element-style-arrays": "warn",
|
||||
|
||||
// Import
|
||||
"import/no-unresolved": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"builtin", // Node "fs", "path", etc.
|
||||
"external", // "react", "lodash", etc.
|
||||
"internal", // Aliased paths like "@/utils"
|
||||
"parent", // "../"
|
||||
"sibling", // "./"
|
||||
"index", // "./index"
|
||||
"object", // import 'foo'
|
||||
"type" // import type ...
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
pattern: "@/entrypoints/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/utils/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/hooks/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
order: "asc",
|
||||
caseInsensitive: true
|
||||
}
|
||||
}
|
||||
],
|
||||
// JSDoc
|
||||
"jsdoc/require-jsdoc": [
|
||||
"error",
|
||||
{
|
||||
require: {
|
||||
FunctionDeclaration: true,
|
||||
MethodDefinition: true,
|
||||
ClassDeclaration: true,
|
||||
ArrowFunctionExpression: true,
|
||||
FunctionExpression: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"jsdoc/require-description": [
|
||||
"error",
|
||||
{
|
||||
contexts: [
|
||||
"FunctionDeclaration",
|
||||
"MethodDefinition",
|
||||
"ClassDeclaration",
|
||||
"ArrowFunctionExpression",
|
||||
"FunctionExpression",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// Style
|
||||
curly: ["error", "all"],
|
||||
"brace-style": ["error", "1tbs", { allowSingleLine: false }],
|
||||
indent: [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
SwitchCase: 1,
|
||||
VariableDeclarator: 1,
|
||||
outerIIFEBody: 1,
|
||||
MemberExpression: 1,
|
||||
FunctionDeclaration: { parameters: 1, body: 1 },
|
||||
FunctionExpression: { parameters: 1, body: 1 },
|
||||
CallExpression: { arguments: 1 },
|
||||
ArrayExpression: 1,
|
||||
ObjectExpression: 1,
|
||||
ImportDeclaration: 1,
|
||||
flatTernaryExpressions: false,
|
||||
ignoreComments: false,
|
||||
},
|
||||
],
|
||||
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1, maxBOF: 0 }],
|
||||
"no-console": ["error", { allow: ["warn", "error", "info", "debug"] }],
|
||||
"spaced-comment": ["error", "always"],
|
||||
"multiline-comment-style": ["error", "starred-block"],
|
||||
|
||||
// TODO: this line is added to prevent "Raw text (×) cannot be used outside of a <Text> tag" errors.
|
||||
// When adding proper i18n multilingual enforcement checks, the following line should be removed
|
||||
'react-native/no-raw-text': 'off',
|
||||
|
||||
// Disable prop-types rule because we're using TypeScript for type-checking
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
};
|
||||
@@ -73,14 +73,14 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 220000
|
||||
versionName "0.22.0"
|
||||
versionCode 230000
|
||||
versionName "0.23.1"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -184,7 +184,7 @@ dependencies {
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
// Add vector drawable support for SVG
|
||||
implementation 'com.caverock:androidsvg:1.4'
|
||||
implementation("com.caverock:androidsvg-aar:1.4")
|
||||
|
||||
// Test dependencies
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true" android:localeConfig="@xml/locales_config">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true" android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="center_vertical">
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/splashscreen_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="center_vertical">
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/splashscreen_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
android:padding="8dp"
|
||||
android:background="@color/splashscreen_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<string name="autofill_open_app">Ouvrir l’application</string>
|
||||
<string name="autofill_vault_locked">Coffre-fort verrouillé</string>
|
||||
<!-- Biometric prompts -->
|
||||
<string name="biometric_store_key_title">Store Encryption Key</string>
|
||||
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
|
||||
<string name="biometric_unlock_vault_title">Unlock Vault</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
|
||||
<string name="biometric_store_key_title">Stocker la clé de chiffrement</string>
|
||||
<string name="biometric_store_key_subtitle">Authentifiez-vous pour stocker votre clé de chiffrement en toute sécurité dans le Keystore Android. Cela permet un accès sécurisé à votre coffre.</string>
|
||||
<string name="biometric_unlock_vault_title">Déverrouiller le coffre</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Authentifiez-vous pour accéder à votre coffre</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">AliasVault</string>
|
||||
<string name="autofill_service_description" translatable="true">השלמה אוטומטית עם AliasVault</string>
|
||||
<string name="aliasvault_icon">סמל AliasVault</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">המשיכה נכשלה, נא לפתוח את היישום</string>
|
||||
<string name="autofill_no_match_found">לא נמצאו התאמות, ליצור חדש?</string>
|
||||
<string name="autofill_open_app">פתיחת היישום</string>
|
||||
<string name="autofill_vault_locked">הכספת נעולה</string>
|
||||
<!-- Biometric prompts -->
|
||||
<string name="biometric_store_key_title">אחסון מפתח הצפנה</string>
|
||||
<string name="biometric_store_key_subtitle">יש לעבור אימות כדי לאחסן בבטחה את מפתח ההצפנה שלך ב־Android Keystore (אחסון מפתחות). כך מופעלת גישה מאובטחת לכספת שלך.</string>
|
||||
<string name="biometric_unlock_vault_title">שחרור נעילת כספת</string>
|
||||
<string name="biometric_unlock_vault_subtitle">יש לעבור אימות כדי לגשת לכספת שלך</string>
|
||||
</resources>
|
||||
@@ -9,8 +9,8 @@
|
||||
<string name="autofill_open_app">Открыть приложение</string>
|
||||
<string name="autofill_vault_locked">Хранилище заблокировано</string>
|
||||
<!-- Biometric prompts -->
|
||||
<string name="biometric_store_key_title">Store Encryption Key</string>
|
||||
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
|
||||
<string name="biometric_unlock_vault_title">Unlock Vault</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
|
||||
<string name="biometric_store_key_title">Храните ключ шифрования</string>
|
||||
<string name="biometric_store_key_subtitle">Пройдите аутентификацию, чтобы надежно сохранить свой ключ шифрования в хранилище ключей Android. Это обеспечивает безопасный доступ к вашему хранилищу.</string>
|
||||
<string name="biometric_unlock_vault_title">Разблокировать хранилище</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Пройдите проверку подлинности, чтобы получить доступ к вашему хранилищу</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system"/>
|
||||
<certificates src="user"/> <!-- opt-in to user-installed CAs -->
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -6,10 +6,10 @@ buildscript {
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '30')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '2.0.21'
|
||||
detektVersion = '1.23.5'
|
||||
|
||||
ndkVersion = "26.1.10909125"
|
||||
ndkVersion = "27.1.12297006"
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
||||
@@ -56,5 +56,4 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
# Workaround for Expo modules compatibility with Android Gradle Plugin 8.x
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
3
apps/mobile-app/android/gradlew
vendored
3
apps/mobile-app/android/gradlew
vendored
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.1",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "net.aliasvault.app",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking, Pressable, Platform } from 'react-native';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, Linking, Platform } from 'react-native'
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -20,6 +20,7 @@ import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
/**
|
||||
@@ -49,32 +50,16 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
{Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleEdit}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleEdit}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<RobustPressable
|
||||
onPress={handleEdit}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={Platform.OS === 'android' ? 24 : 22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
@@ -137,11 +122,13 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
</ThemedText>
|
||||
{credential.ServiceUrl && (
|
||||
/^https?:\/\//i.test(credential.ServiceUrl) ? (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(credential.ServiceUrl!)}>
|
||||
<RobustPressable
|
||||
onPress={() => Linking.openURL(credential.ServiceUrl!)}
|
||||
>
|
||||
<Text style={[styles.serviceUrl, { color: colors.primary }]}>
|
||||
{credential.ServiceUrl}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
) : (
|
||||
<Text style={styles.serviceUrl}>
|
||||
{credential.ServiceUrl}
|
||||
@@ -170,6 +157,10 @@ const styles = StyleSheet.create({
|
||||
headerRightButton: {
|
||||
padding: 10,
|
||||
},
|
||||
headerRightButtonPressed: {
|
||||
padding: 10,
|
||||
opacity: 0.8,
|
||||
},
|
||||
headerRightContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
@@ -5,12 +7,12 @@ import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-rout
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingView, Platform, Pressable } from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import { StyleSheet, View, Alert, Keyboard, Platform } from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { CreateIdentityGenerator, IdentityGenerator, IdentityHelperUtils } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential, PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
|
||||
import type { FaviconExtractModel } from '@/utils/dist/shared/models/webapi';
|
||||
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
@@ -28,6 +30,7 @@ import LoadingOverlay from '@/components/LoadingOverlay';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { AliasVaultToast } from '@/components/Toast';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
@@ -53,7 +56,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const [passwordSettings, setPasswordSettings] = useState<PasswordSettings | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control, handleSubmit, setValue, watch } = useForm<Credential>({
|
||||
@@ -133,14 +135,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load password settings
|
||||
try {
|
||||
const settings = await dbContext.sqliteClient!.getPasswordSettings();
|
||||
setPasswordSettings(settings);
|
||||
} catch (err) {
|
||||
console.error('Error loading password settings:', err);
|
||||
}
|
||||
|
||||
if (isEditMode) {
|
||||
loadExistingCredential();
|
||||
} else if (serviceUrl) {
|
||||
@@ -328,7 +322,11 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
|
||||
// Then navigate after a short delay to ensure the modal has closed
|
||||
setTimeout(() => {
|
||||
router.push(`/credentials/${credentialToSave.Id}`);
|
||||
if (isEditMode) {
|
||||
// Do nothing, as the original screen will update itself.
|
||||
} else {
|
||||
router.push(`/credentials/${credentialToSave.Id}`);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Show success toast
|
||||
@@ -369,25 +367,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random password.
|
||||
*/
|
||||
const generateRandomPassword = async () : Promise<void> => {
|
||||
try {
|
||||
const { passwordGenerator } = await initializeGenerators();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setValue('Password', password);
|
||||
setIsPasswordVisible(true);
|
||||
} catch (error) {
|
||||
console.error('Error generating random password:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('credentials.errors.generatePasswordFailed'),
|
||||
text2: t('auth.errors.enterPassword')
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the delete button press.
|
||||
*/
|
||||
@@ -536,51 +515,40 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
|
||||
// Set header buttons
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'ios') {
|
||||
navigation.setOptions({
|
||||
navigation.setOptions({
|
||||
/**
|
||||
* Header left button (iOS only).
|
||||
*/
|
||||
...(Platform.OS === 'ios' && {
|
||||
/**
|
||||
* Header left button.
|
||||
*/
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
headerLeft: () : React.ReactNode => (
|
||||
<RobustPressable
|
||||
onPress={() => router.back()}
|
||||
style={styles.headerLeftButton}
|
||||
>
|
||||
<ThemedText style={styles.headerLeftButtonText}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
),
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<MaterialIcons name="save" size={22} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
navigation.setOptions({
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPressIn={handleSubmit(onSubmit)}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<MaterialIcons name="save" size={24} color={colors.primary} />
|
||||
</Pressable>
|
||||
),
|
||||
});
|
||||
}
|
||||
}),
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () => (
|
||||
<RobustPressable
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="save"
|
||||
size={Platform.OS === 'android' ? 24 : 22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</RobustPressable>
|
||||
),
|
||||
});
|
||||
}, [navigation, mode, handleSubmit, onSubmit, colors.primary, isEditMode, router, styles.headerLeftButton, styles.headerLeftButtonText, styles.headerRightButton, styles.headerRightButtonDisabled, isSaveDisabled, t]);
|
||||
|
||||
return (
|
||||
@@ -589,188 +557,164 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
{(isSyncing) && (
|
||||
<LoadingOverlay status={syncStatus} />
|
||||
)}
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardContainer}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
<ThemedContainer style={styles.container}>
|
||||
<KeyboardAwareScrollView
|
||||
enableOnAndroid={true}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
extraScrollHeight={0}
|
||||
>
|
||||
{!isEditMode && (
|
||||
<View style={styles.modeSelector}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeButton, mode === 'random' && styles.modeButtonActive]}
|
||||
onPress={() => setMode('random')}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="auto-fix-high"
|
||||
size={20}
|
||||
color={mode === 'random' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'random' && styles.modeButtonTextActive]}>
|
||||
{t('credentials.randomAlias')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modeButton, mode === 'manual' && styles.modeButtonActive]}
|
||||
onPress={() => setMode('manual')}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="person"
|
||||
size={20}
|
||||
color={mode === 'manual' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'manual' && styles.modeButtonTextActive]}>
|
||||
{t('credentials.manual')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.service')}</ThemedText>
|
||||
<ValidatedFormField
|
||||
ref={serviceNameRef}
|
||||
control={control}
|
||||
name="ServiceName"
|
||||
label={t('credentials.serviceName')}
|
||||
required
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="ServiceUrl"
|
||||
label={t('credentials.serviceUrl')}
|
||||
/>
|
||||
<ThemedContainer style={styles.container}>
|
||||
<KeyboardAwareScrollView
|
||||
enabled={true}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bottomOffset={30}
|
||||
>
|
||||
{!isEditMode && (
|
||||
<View style={styles.modeSelector}>
|
||||
<RobustPressable
|
||||
style={[styles.modeButton, mode === 'random' && styles.modeButtonActive]}
|
||||
onPress={() => setMode('random')}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="auto-fix-high"
|
||||
size={20}
|
||||
color={mode === 'random' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'random' && styles.modeButtonTextActive]}>
|
||||
{t('credentials.randomAlias')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
<RobustPressable
|
||||
style={[styles.modeButton, mode === 'manual' && styles.modeButtonActive]}
|
||||
onPress={() => setMode('manual')}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="person"
|
||||
size={20}
|
||||
color={mode === 'manual' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'manual' && styles.modeButtonTextActive]}>
|
||||
{t('credentials.manual')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.loginCredentials')}</ThemedText>
|
||||
)}
|
||||
|
||||
<EmailDomainField
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(newValue) => setValue('Alias.Email', newValue)}
|
||||
label={t('credentials.email')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Username"
|
||||
label={t('credentials.username')}
|
||||
buttons={[
|
||||
{
|
||||
icon: "refresh",
|
||||
onPress: generateRandomUsername
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{passwordSettings ? (
|
||||
<AdvancedPasswordField
|
||||
control={control}
|
||||
name="Password"
|
||||
label={t('credentials.password')}
|
||||
initialSettings={passwordSettings}
|
||||
showPassword={isPasswordVisible}
|
||||
onShowPasswordChange={setIsPasswordVisible}
|
||||
isNewCredential={!isEditMode}
|
||||
/>
|
||||
) : (
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Password"
|
||||
label={t('credentials.password')}
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
buttons={[
|
||||
{
|
||||
icon: isPasswordVisible ? "visibility-off" : "visibility",
|
||||
/**
|
||||
* Toggle the visibility of the password.
|
||||
*/
|
||||
onPress: () => setIsPasswordVisible(!isPasswordVisible)
|
||||
},
|
||||
{
|
||||
icon: "refresh",
|
||||
onPress: generateRandomPassword
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.service')}</ThemedText>
|
||||
<ValidatedFormField
|
||||
ref={serviceNameRef}
|
||||
control={control}
|
||||
name="ServiceName"
|
||||
label={t('credentials.serviceName')}
|
||||
required
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="ServiceUrl"
|
||||
label={t('credentials.serviceUrl')}
|
||||
/>
|
||||
</View>
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.loginCredentials')}</ThemedText>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.alias')}</ThemedText>
|
||||
<TouchableOpacity style={styles.generateButton} onPress={handleGenerateRandomAlias}>
|
||||
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
|
||||
<ThemedText style={styles.generateButtonText}>{t('credentials.generateRandomAlias')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.FirstName"
|
||||
label={t('credentials.firstName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.LastName"
|
||||
label={t('credentials.lastName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.NickName"
|
||||
label={t('credentials.nickName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.Gender"
|
||||
label={t('credentials.gender')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.BirthDate"
|
||||
label={t('credentials.birthDate')}
|
||||
placeholder={t('credentials.birthDatePlaceholder')}
|
||||
/>
|
||||
</View>
|
||||
<EmailDomainField
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(newValue) => setValue('Alias.Email', newValue)}
|
||||
label={t('credentials.email')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Username"
|
||||
label={t('credentials.username')}
|
||||
buttons={[
|
||||
{
|
||||
icon: "refresh",
|
||||
onPress: generateRandomUsername
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<AdvancedPasswordField
|
||||
control={control}
|
||||
name="Password"
|
||||
label={t('credentials.password')}
|
||||
showPassword={isPasswordVisible}
|
||||
onShowPasswordChange={setIsPasswordVisible}
|
||||
isNewCredential={!isEditMode}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.metadata')}</ThemedText>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.alias')}</ThemedText>
|
||||
<RobustPressable
|
||||
style={styles.generateButton}
|
||||
onPress={handleGenerateRandomAlias}
|
||||
>
|
||||
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
|
||||
<ThemedText style={styles.generateButtonText}>{t('credentials.generateRandomAlias')}</ThemedText>
|
||||
</RobustPressable>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.FirstName"
|
||||
label={t('credentials.firstName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.LastName"
|
||||
label={t('credentials.lastName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.NickName"
|
||||
label={t('credentials.nickName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.Gender"
|
||||
label={t('credentials.gender')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.BirthDate"
|
||||
label={t('credentials.birthDate')}
|
||||
placeholder={t('credentials.birthDatePlaceholder')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Notes"
|
||||
label={t('credentials.notes')}
|
||||
multiline={true}
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
{/* TODO: Add TOTP management */}
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.metadata')}</ThemedText>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.attachments')}</ThemedText>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Notes"
|
||||
label={t('credentials.notes')}
|
||||
multiline={true}
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
{/* TODO: Add TOTP management */}
|
||||
</View>
|
||||
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.attachments')}</ThemedText>
|
||||
|
||||
{isEditMode && (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
<ThemedText style={styles.deleteButtonText}>{t('credentials.deleteCredential')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</ThemedContainer>
|
||||
<AliasVaultToast />
|
||||
</KeyboardAvoidingView>
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isEditMode && (
|
||||
<RobustPressable
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
<ThemedText style={styles.deleteButtonText}>{t('credentials.deleteCredential')}</ThemedText>
|
||||
</RobustPressable>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</ThemedContainer>
|
||||
<AliasVaultToast />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, AppState, Platform, Pressable } from 'react-native';
|
||||
import { StyleSheet, View, AppState } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedSafeAreaView } from '@/components/themed/ThemedSafeAreaView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
|
||||
/**
|
||||
* Autofill credential created screen.
|
||||
@@ -80,25 +81,15 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () =>
|
||||
Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleStayInApp}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () =>
|
||||
<RobustPressable
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.close')}</ThemedText>
|
||||
</RobustPressable>
|
||||
});
|
||||
}, [navigation, colors.primary, styles.headerRightButton, handleStayInApp, t]);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { AndroidHeader } from '@/components/ui/AndroidHeader';
|
||||
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { SkeletonLoader } from '@/components/ui/SkeletonLoader';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
@@ -172,7 +173,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
setIsLoadingCredentials(false);
|
||||
|
||||
// Show modal with error message
|
||||
Alert.alert(t('credentials.errors.generic'), error);
|
||||
Alert.alert(t('common.error'), error);
|
||||
|
||||
// Logout user
|
||||
await webApi.logout(error);
|
||||
@@ -349,16 +350,15 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
showNavigationHeader={true}
|
||||
alwaysVisible={true}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.fab}
|
||||
onPress={() => {
|
||||
router.push('/(tabs)/credentials/add-edit');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons name="add" style={styles.fabIcon} />
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<Animated.FlatList
|
||||
ref={flatListRef}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { useLocalSearchParams, useRouter, useNavigation, Stack } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, Linking, Text, TextInput, Platform, Pressable } from 'react-native';
|
||||
import { StyleSheet, View, ActivityIndicator, Alert, Share, useColorScheme, Linking, Text, TextInput, Platform } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -17,6 +19,7 @@ import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { IconSymbolName } from '@/components/ui/IconSymbolName';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
@@ -72,7 +75,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
setHtmlView(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.generic'));
|
||||
setError(err instanceof Error ? err.message : t('common.error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -224,7 +227,10 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
},
|
||||
headerRightButton: {
|
||||
padding: 10,
|
||||
paddingRight: 0,
|
||||
paddingRight: 10,
|
||||
},
|
||||
headerRightButtonDelete: {
|
||||
paddingRight: Platform.OS === 'ios' ? 0 : 10,
|
||||
},
|
||||
headerRightContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -324,49 +330,34 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
{Platform.OS === 'android' ? (
|
||||
<>
|
||||
<Pressable
|
||||
onPressIn={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPressIn={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={24} color="#FF0000" />
|
||||
</Pressable>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF0000" />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
<RobustPressable
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
pressRetentionOffset={5}
|
||||
hitSlop={5}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={Platform.OS === 'android' ? 24 : 22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</RobustPressable>
|
||||
<RobustPressable
|
||||
onPress={handleDelete}
|
||||
style={[styles.headerRightButton, styles.headerRightButtonDelete]}
|
||||
pressRetentionOffset={5}
|
||||
hitSlop={5}
|
||||
>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={Platform.OS === 'android' ? 24 : 22}
|
||||
color="#FF0000"
|
||||
/>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [isHtmlView, navigation, handleDelete, colors.primary, styles.headerRightButton, styles.headerRightContainer]);
|
||||
}, [isHtmlView, navigation, handleDelete, colors.primary, styles.headerRightButton, styles.headerRightButtonDelete, styles.headerRightContainer]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -396,7 +387,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
let metadataView = null;
|
||||
if (!isMetadataMaximized) {
|
||||
metadataView = (
|
||||
<TouchableOpacity onPress={() => setMetadataMaximized(!isMetadataMaximized)}>
|
||||
<RobustPressable onPress={() => setMetadataMaximized(!isMetadataMaximized)}>
|
||||
<View style={styles.topBox}>
|
||||
<View style={styles.subjectContainer}>
|
||||
<ThemedText style={styles.subject}>{email.subject}</ThemedText>
|
||||
@@ -405,18 +396,18 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
<Ionicons name="reorder-four-outline" size={22} color={isDarkMode ? '#eee' : '#000'} />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
);
|
||||
} else {
|
||||
metadataView = (
|
||||
<TouchableOpacity onPress={() => setMetadataMaximized(!isMetadataMaximized)}>
|
||||
<RobustPressable onPress={() => setMetadataMaximized(!isMetadataMaximized)}>
|
||||
<View style={styles.metadataContainer}>
|
||||
<View style={styles.metadataRow}>
|
||||
<View style={styles.metadataValue}>
|
||||
<ThemedText style={[styles.metadataText, styles.metadataSubject]}>{email.subject}</ThemedText>
|
||||
{associatedCredential && (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
onPress={handleOpenCredential}
|
||||
style={styles.metadataCredential}
|
||||
>
|
||||
@@ -424,7 +415,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
<ThemedText style={[styles.metadataText, { color: colors.primary }]}>
|
||||
{associatedCredential.ServiceName}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -470,7 +461,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
</View>
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -517,7 +508,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
<View style={styles.attachments}>
|
||||
<ThemedText style={styles.attachmentsTitle}>{t('emails.attachments')}</ThemedText>
|
||||
{email.attachments.map((attachment) => (
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
key={attachment.id}
|
||||
style={styles.attachment}
|
||||
onPress={() => handleDownloadAttachment(attachment)}
|
||||
@@ -526,7 +517,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
<ThemedText style={styles.attachmentName}>
|
||||
{attachment.filename} ({Math.ceil(attachment.filesize / 1024)} {t('emails.sizeKB')})
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.generic'));
|
||||
setError(err instanceof Error ? err.message : t('common.error'));
|
||||
}
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, authContext.isOffline, t]);
|
||||
|
||||
|
||||
@@ -67,6 +67,14 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="password-generator"
|
||||
options={{
|
||||
title: t('settings.passwordGenerator'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="identity-generator"
|
||||
options={{
|
||||
@@ -74,6 +82,14 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="import-export"
|
||||
options={{
|
||||
title: t('settings.importExport'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/index"
|
||||
options={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, TouchableOpacity } from 'react-native';
|
||||
|
||||
@@ -26,6 +26,10 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
const [language, setLanguage] = useState<string>('en');
|
||||
const [gender, setGender] = useState<string>('random');
|
||||
|
||||
// Store pending changes and initial values
|
||||
const pendingChanges = useRef<{ language?: string; gender?: string }>({});
|
||||
const initialValues = useRef<{ language: string; gender: string }>({ language: 'en', gender: 'random' });
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ label: t('settings.identityGeneratorSettings.languageOptions.english'), value: 'en' },
|
||||
{ label: t('settings.identityGeneratorSettings.languageOptions.dutch'), value: 'nl' }
|
||||
@@ -40,7 +44,7 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
/**
|
||||
* Load the identity generator settings.
|
||||
* Load the identity generator settings on focus.
|
||||
*/
|
||||
const loadSettings = async (): Promise<void> => {
|
||||
try {
|
||||
@@ -51,46 +55,71 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
|
||||
setLanguage(currentLanguage);
|
||||
setGender(currentGender);
|
||||
// Store initial values
|
||||
initialValues.current = { language: currentLanguage, gender: currentGender };
|
||||
// Clear pending changes when screen loads
|
||||
pendingChanges.current = {};
|
||||
} catch (error) {
|
||||
console.error('Error loading identity generator settings:', error);
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.loadFailed'));
|
||||
Alert.alert(t('common.error'), t('common.unknownError'));
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [dbContext.sqliteClient, t])
|
||||
|
||||
// Save changes when screen loses focus (navigating away)
|
||||
return (): void => {
|
||||
/**
|
||||
* Save pending changes to the database.
|
||||
*/
|
||||
const saveChanges = async (): Promise<void> => {
|
||||
// Check if there are pending changes to save
|
||||
const hasChanges = Object.keys(pendingChanges.current).length > 0;
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save all pending changes in a single vault mutation
|
||||
await executeVaultMutation(async () => {
|
||||
if (pendingChanges.current.language !== undefined) {
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityLanguage', pendingChanges.current.language);
|
||||
}
|
||||
if (pendingChanges.current.gender !== undefined) {
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityGender', pendingChanges.current.gender);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear pending changes after successful save
|
||||
pendingChanges.current = {};
|
||||
} catch (error) {
|
||||
console.error('Error saving identity generator settings:', error);
|
||||
// Don't show alert when navigating away to avoid blocking navigation
|
||||
}
|
||||
};
|
||||
|
||||
// Execute save without blocking navigation
|
||||
saveChanges();
|
||||
};
|
||||
}, [dbContext.sqliteClient, t, executeVaultMutation])
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle language change.
|
||||
* Handle language change - just update UI and store pending change.
|
||||
*/
|
||||
const handleLanguageChange = useCallback(async (newLanguage: string): Promise<void> => {
|
||||
try {
|
||||
executeVaultMutation(async () => {
|
||||
// Update the default language setting
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityLanguage', newLanguage);
|
||||
});
|
||||
setLanguage(newLanguage);
|
||||
} catch (error) {
|
||||
console.error('Error updating language setting:', error);
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.languageUpdateFailed'));
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient, t]);
|
||||
const handleLanguageChange = useCallback((newLanguage: string): void => {
|
||||
setLanguage(newLanguage);
|
||||
pendingChanges.current.language = newLanguage;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle gender change.
|
||||
* Handle gender change - just update UI and store pending change.
|
||||
*/
|
||||
const handleGenderChange = useCallback(async (newGender: string): Promise<void> => {
|
||||
try {
|
||||
executeVaultMutation(async () => {
|
||||
await dbContext.sqliteClient!.updateSetting('DefaultIdentityGender', newGender);
|
||||
});
|
||||
setGender(newGender);
|
||||
} catch (error) {
|
||||
console.error('Error updating gender setting:', error);
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.genderUpdateFailed'));
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient, t]);
|
||||
const handleGenderChange = useCallback((newGender: string): void => {
|
||||
setGender(newGender);
|
||||
pendingChanges.current.gender = newGender;
|
||||
}, []);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
descriptionText: {
|
||||
|
||||
375
apps/mobile-app/app/(tabs)/settings/import-export.tsx
Normal file
375
apps/mobile-app/app/(tabs)/settings/import-export.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useState } from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert } from 'react-native';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
/**
|
||||
* CSV record for Credential objects (matching server format).
|
||||
*/
|
||||
interface ICredentialCsvRecord {
|
||||
Version: string;
|
||||
Username: string;
|
||||
Notes: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
AliasGender: string;
|
||||
AliasFirstName: string;
|
||||
AliasLastName: string;
|
||||
AliasNickName: string;
|
||||
AliasBirthDate: string;
|
||||
AliasEmail: string;
|
||||
ServiceName: string;
|
||||
ServiceUrl: string;
|
||||
CurrentPassword: string;
|
||||
TwoFactorSecret: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import/Export settings screen.
|
||||
* @returns The Import/Export settings screen component.
|
||||
*/
|
||||
export default function ImportExportScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
/**
|
||||
* Format date to match server format (MM/DD/YYYY HH:mm:ss).
|
||||
*/
|
||||
const formatDate = (dateStr: string | null | undefined): string => {
|
||||
if (!dateStr) {
|
||||
const now = new Date();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const year = now.getFullYear();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) {
|
||||
// If invalid date, return a default date
|
||||
return '01/01/0001 00:00:00';
|
||||
}
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}`;
|
||||
} catch {
|
||||
// Return default date if parsing fails
|
||||
return '01/01/0001 00:00:00';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert credentials to CSV format.
|
||||
*/
|
||||
const credentialsToCsv = async (credentials: Credential[]): Promise<string> => {
|
||||
const records: ICredentialCsvRecord[] = [];
|
||||
|
||||
// Get all credentials with their TOTP codes
|
||||
for (const credential of credentials) {
|
||||
// Get TOTP codes for this credential
|
||||
const totpCodes = await dbContext.sqliteClient?.getTotpCodesForCredential(credential.Id) ?? [];
|
||||
const totpSecret = totpCodes.length > 0 ? totpCodes[0].SecretKey : '';
|
||||
|
||||
/*
|
||||
* For now, we'll use current date for CreatedAt/UpdatedAt since they're not available
|
||||
* in the Credential type. In a production scenario, we'd want to extend the
|
||||
* SqliteClient to fetch these fields.
|
||||
*/
|
||||
const currentDate = formatDate(new Date().toISOString());
|
||||
|
||||
const record: ICredentialCsvRecord = {
|
||||
Version: '1.5.0',
|
||||
Username: credential.Username ?? '',
|
||||
Notes: credential.Notes ?? '',
|
||||
CreatedAt: currentDate,
|
||||
UpdatedAt: currentDate,
|
||||
AliasGender: credential.Alias?.Gender ?? '',
|
||||
AliasFirstName: credential.Alias?.FirstName ?? '',
|
||||
AliasLastName: credential.Alias?.LastName ?? '',
|
||||
AliasNickName: credential.Alias?.NickName ?? '',
|
||||
AliasBirthDate: credential.Alias?.BirthDate ? formatDate(credential.Alias.BirthDate) : '01/01/0001 00:00:00',
|
||||
AliasEmail: credential.Alias?.Email ?? '',
|
||||
ServiceName: credential.ServiceName ?? '',
|
||||
ServiceUrl: credential.ServiceUrl ?? '',
|
||||
CurrentPassword: credential.Password ?? '',
|
||||
TwoFactorSecret: totpSecret
|
||||
};
|
||||
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
// Generate CSV header
|
||||
const headers = [
|
||||
'Version',
|
||||
'Username',
|
||||
'Notes',
|
||||
'CreatedAt',
|
||||
'UpdatedAt',
|
||||
'AliasGender',
|
||||
'AliasFirstName',
|
||||
'AliasLastName',
|
||||
'AliasNickName',
|
||||
'AliasBirthDate',
|
||||
'AliasEmail',
|
||||
'ServiceName',
|
||||
'ServiceUrl',
|
||||
'CurrentPassword',
|
||||
'TwoFactorSecret'
|
||||
];
|
||||
|
||||
/**
|
||||
* Escape CSV value.
|
||||
* @param {string} value - The value to escape.
|
||||
* @returns {string} The escaped value.
|
||||
*/
|
||||
const escapeCsvValue = (value: string): string => {
|
||||
// If value contains comma, newline, or quote, wrap in quotes
|
||||
if (value.includes(',') || value.includes('\n') || value.includes('"') || value.includes('\r')) {
|
||||
// Escape quotes by doubling them and wrap in quotes
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// Generate CSV content
|
||||
const csvLines: string[] = [headers.join(',')];
|
||||
|
||||
for (const record of records) {
|
||||
const values = [
|
||||
record.Version,
|
||||
escapeCsvValue(record.Username),
|
||||
escapeCsvValue(record.Notes),
|
||||
record.CreatedAt,
|
||||
record.UpdatedAt,
|
||||
record.AliasGender,
|
||||
record.AliasFirstName,
|
||||
record.AliasLastName,
|
||||
record.AliasNickName,
|
||||
record.AliasBirthDate,
|
||||
record.AliasEmail,
|
||||
escapeCsvValue(record.ServiceName),
|
||||
escapeCsvValue(record.ServiceUrl),
|
||||
escapeCsvValue(record.CurrentPassword),
|
||||
escapeCsvValue(record.TwoFactorSecret)
|
||||
];
|
||||
csvLines.push(values.join(','));
|
||||
}
|
||||
|
||||
return csvLines.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show export confirmation dialog.
|
||||
*/
|
||||
const showExportConfirmation = (): void => {
|
||||
const warningMessage = t('settings.exportWarning');
|
||||
|
||||
Alert.alert(
|
||||
t('settings.exportConfirmTitle'),
|
||||
warningMessage,
|
||||
[
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: t('common.confirm'),
|
||||
style: 'destructive',
|
||||
/**
|
||||
* Handle export confirmation.
|
||||
*/
|
||||
onPress: (): void => {
|
||||
handleExport();
|
||||
}
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the CSV export.
|
||||
*/
|
||||
const handleExport = async (): Promise<void> => {
|
||||
if (isExporting) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: when updating this CSV export logic, make sure to update the
|
||||
* unittest "ImportCredentialsFromAliasVaultMobileAppCsv" in the .NET solution as well.
|
||||
*/
|
||||
|
||||
setIsExporting(true);
|
||||
|
||||
try {
|
||||
const dateStr = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Export as CSV
|
||||
const credentials = await dbContext.sqliteClient?.getAllCredentials() ?? [];
|
||||
const csvContent = await credentialsToCsv(credentials);
|
||||
|
||||
const filename = `aliasvault-export-${dateStr}.csv`;
|
||||
const downloadsDir = FileSystem.documentDirectory + 'Exports/';
|
||||
const filePath = downloadsDir + filename;
|
||||
|
||||
// Ensure Exports directory exists
|
||||
const dirInfo = await FileSystem.getInfoAsync(downloadsDir);
|
||||
if (!dirInfo.exists) {
|
||||
await FileSystem.makeDirectoryAsync(downloadsDir, { intermediates: true });
|
||||
}
|
||||
|
||||
// Write CSV file
|
||||
await FileSystem.writeAsStringAsync(filePath, csvContent, {
|
||||
encoding: FileSystem.EncodingType.UTF8,
|
||||
});
|
||||
|
||||
// Share the file using the system share dialog
|
||||
const canShare = await Sharing.isAvailableAsync();
|
||||
if (canShare) {
|
||||
await Sharing.shareAsync(filePath, {
|
||||
dialogTitle: filename,
|
||||
mimeType: 'text/csv',
|
||||
});
|
||||
|
||||
// Clean up the temporary file after sharing
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await FileSystem.deleteAsync(filePath, { idempotent: true });
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up export file:', error);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
t('common.unknownError')
|
||||
);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
section: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 16,
|
||||
marginHorizontal: 16,
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
color: colors.text,
|
||||
},
|
||||
sectionDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.textMuted,
|
||||
marginBottom: 16,
|
||||
lineHeight: 20,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
},
|
||||
importNote: {
|
||||
backgroundColor: colors.tertiary + '20', // Use tertiary color with opacity
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
importNoteText: {
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
{/* Import Section */}
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>
|
||||
{t('settings.importSectionTitle')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.sectionDescription}>
|
||||
{t('settings.importSectionDescription')}
|
||||
</ThemedText>
|
||||
<View style={styles.importNote}>
|
||||
<ThemedText style={styles.importNoteText}>
|
||||
{t('settings.importWebNote')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Export Section */}
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>
|
||||
{t('settings.exportSectionTitle')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.sectionDescription}>
|
||||
{t('settings.exportSectionDescription')}
|
||||
</ThemedText>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isExporting && styles.buttonDisabled]}
|
||||
onPress={() => showExportConfirmation()}
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Ionicons name="document-text" size={20} color={colors.primarySurfaceText} />
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isExporting
|
||||
? (t('settings.exporting'))
|
||||
: (t('settings.exportCsvButton'))
|
||||
}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
@@ -164,6 +164,13 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
router.push('/(tabs)/settings/identity-generator');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the password generator settings press.
|
||||
*/
|
||||
const handlePasswordGeneratorPress = () : void => {
|
||||
router.push('/(tabs)/settings/password-generator');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the clipboard clear settings press.
|
||||
*/
|
||||
@@ -223,7 +230,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
paddingBottom: 80,
|
||||
paddingTop: Platform.OS === 'ios' ? 42 : 16,
|
||||
},
|
||||
scrollView: {
|
||||
@@ -435,6 +442,19 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={handlePasswordGeneratorPress}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="key-sharp" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.passwordGenerator')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={handleIdentityGeneratorPress}
|
||||
@@ -448,6 +468,19 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/(tabs)/settings/import-export')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="swap-horizontal" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.importExport')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/(tabs)/settings/security')}
|
||||
|
||||
413
apps/mobile-app/app/(tabs)/settings/password-generator.tsx
Normal file
413
apps/mobile-app/app/(tabs)/settings/password-generator.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, TouchableOpacity, Switch, Platform } from 'react-native';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
/**
|
||||
* Password Generator Settings screen.
|
||||
*/
|
||||
export default function PasswordGeneratorSettingsScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
|
||||
const [settings, setSettings] = useState<PasswordSettings | null>(null);
|
||||
const [previewPassword, setPreviewPassword] = useState<string>('');
|
||||
const [sliderValue, setSliderValue] = useState<number | null>(null);
|
||||
|
||||
// Store pending changes and initial values
|
||||
const pendingChanges = useRef<Partial<PasswordSettings>>({});
|
||||
const lastGeneratedLength = useRef<number>(0);
|
||||
const isSliding = useRef(false);
|
||||
const initialValues = useRef<PasswordSettings | null>(null);
|
||||
|
||||
const handleRefreshPreview = useCallback(() => {
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
/**
|
||||
* Load the password generator settings on focus.
|
||||
*/
|
||||
const loadSettings = async (): Promise<void> => {
|
||||
try {
|
||||
const passwordSettings = await dbContext.sqliteClient!.getPasswordSettings();
|
||||
|
||||
setSettings(passwordSettings);
|
||||
setSliderValue(passwordSettings.Length);
|
||||
initialValues.current = passwordSettings;
|
||||
|
||||
// Generate initial preview password only once
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating initial password:', error);
|
||||
setPreviewPassword('');
|
||||
}
|
||||
|
||||
// Clear pending changes when screen loads
|
||||
pendingChanges.current = {};
|
||||
console.debug('Settings loaded and initialized');
|
||||
} catch (error) {
|
||||
console.error('Error loading password generator settings:', error);
|
||||
Alert.alert(t('common.error'), t('common.unknownError'));
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
|
||||
// Save changes when screen loses focus (navigating away)
|
||||
return (): void => {
|
||||
const hasChanges = Object.keys(pendingChanges.current).length > 0;
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the merged settings with all pending changes
|
||||
if (!initialValues.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalSettings = { ...initialValues.current, ...pendingChanges.current };
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
// Save as JSON serialized object
|
||||
const settingsJson = JSON.stringify(finalSettings);
|
||||
await dbContext.sqliteClient!.updateSetting('PasswordGenerationSettings', settingsJson);
|
||||
}).then(() => {
|
||||
// Update initial values after successful save
|
||||
initialValues.current = finalSettings;
|
||||
// Clear pending changes after successful save
|
||||
pendingChanges.current = {};
|
||||
}).catch((error) => {
|
||||
console.error('Error saving password generator settings:', error);
|
||||
});
|
||||
};
|
||||
}, [dbContext.sqliteClient, t, executeVaultMutation])
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle slider value change.
|
||||
*/
|
||||
const handleSliderChange = useCallback((value: number): void => {
|
||||
const roundedLength = Math.round(value);
|
||||
setSliderValue(roundedLength);
|
||||
|
||||
// Only generate if value actually changed and we're actively sliding
|
||||
if (roundedLength !== lastGeneratedLength.current && isSliding.current && settings) {
|
||||
lastGeneratedLength.current = roundedLength;
|
||||
|
||||
// Update settings and regenerate password
|
||||
const newSettings = { ...settings, Length: roundedLength };
|
||||
setSettings(newSettings);
|
||||
|
||||
// Track the change
|
||||
pendingChanges.current = { ...pendingChanges.current, Length: roundedLength };
|
||||
|
||||
// Generate new preview password
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(newSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
}
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
/**
|
||||
* Handle slider drag start.
|
||||
*/
|
||||
const handleSliderStart = useCallback((): void => {
|
||||
isSliding.current = true;
|
||||
lastGeneratedLength.current = sliderValue ?? 0;
|
||||
}, [sliderValue]);
|
||||
|
||||
/**
|
||||
* Handle slider drag complete.
|
||||
*/
|
||||
const handleSliderComplete = useCallback((value: number): void => {
|
||||
isSliding.current = false;
|
||||
const roundedLength = Math.round(value);
|
||||
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update settings with final value
|
||||
const newSettings = { ...settings, Length: roundedLength };
|
||||
setSettings(newSettings);
|
||||
|
||||
// Track the change
|
||||
pendingChanges.current = { ...pendingChanges.current, Length: roundedLength };
|
||||
|
||||
// Generate password with final value
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(newSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
}
|
||||
|
||||
lastGeneratedLength.current = 0;
|
||||
}, [settings]);
|
||||
|
||||
/**
|
||||
* Update a boolean setting.
|
||||
*/
|
||||
const updateSetting = useCallback((key: keyof PasswordSettings, value: boolean): void => {
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
const newSettings = { ...settings, [key]: value };
|
||||
setSettings(newSettings);
|
||||
|
||||
// Track the change
|
||||
pendingChanges.current = { ...pendingChanges.current, [key]: value };
|
||||
|
||||
// Generate new preview password
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(newSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
descriptionText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
headerText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
marginBottom: 8,
|
||||
},
|
||||
previewContainer: {
|
||||
marginBottom: 20,
|
||||
marginTop: 16,
|
||||
},
|
||||
previewInput: {
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
||||
fontSize: 14,
|
||||
padding: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
previewInputContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderColor: colors.accentBorder,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
previewLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
},
|
||||
refreshButton: {
|
||||
borderLeftColor: colors.accentBorder,
|
||||
borderLeftWidth: 1,
|
||||
padding: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
marginTop: 20,
|
||||
},
|
||||
settingItem: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: colors.accentBorder,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
settingItemLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
settingLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
},
|
||||
settingsContainer: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
slider: {
|
||||
height: 40,
|
||||
width: '100%',
|
||||
},
|
||||
sliderContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
sliderHeader: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
},
|
||||
sliderLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
sliderValue: {
|
||||
color: colors.primary,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
// Don't render until settings are loaded
|
||||
if (!settings) {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ThemedText>{t('common.loading')}</ThemedText>
|
||||
</View>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
{t('settings.passwordGeneratorSettings.description')}
|
||||
</ThemedText>
|
||||
|
||||
<View style={styles.previewContainer}>
|
||||
<ThemedText style={styles.previewLabel}>{t('settings.passwordGeneratorSettings.preview')}</ThemedText>
|
||||
<View style={styles.previewInputContainer}>
|
||||
<ThemedText style={styles.previewInput}>{previewPassword}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={styles.refreshButton}
|
||||
onPress={handleRefreshPreview}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="refresh" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingsContainer}>
|
||||
<View style={styles.sliderContainer}>
|
||||
<View style={styles.sliderHeader}>
|
||||
<ThemedText style={styles.sliderLabel}>{t('credentials.passwordLength')}</ThemedText>
|
||||
<ThemedText style={styles.sliderValue}>{sliderValue ?? 0}</ThemedText>
|
||||
</View>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
minimumValue={8}
|
||||
maximumValue={64}
|
||||
value={sliderValue ?? 0}
|
||||
onValueChange={handleSliderChange}
|
||||
onSlidingStart={handleSliderStart}
|
||||
onSlidingComplete={handleSliderComplete}
|
||||
step={1}
|
||||
minimumTrackTintColor={colors.primary}
|
||||
maximumTrackTintColor={colors.accentBorder}
|
||||
thumbTintColor={colors.primary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingsContainer}>
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeLowercase')}</ThemedText>
|
||||
<Switch
|
||||
value={settings.UseLowercase}
|
||||
onValueChange={(value) => updateSetting('UseLowercase', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeUppercase')}</ThemedText>
|
||||
<Switch
|
||||
value={settings.UseUppercase}
|
||||
onValueChange={(value) => updateSetting('UseUppercase', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeNumbers')}</ThemedText>
|
||||
<Switch
|
||||
value={settings.UseNumbers}
|
||||
onValueChange={(value) => updateSetting('UseNumbers', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeSpecialChars')}</ThemedText>
|
||||
<Switch
|
||||
value={settings.UseSpecialChars}
|
||||
onValueChange={(value) => updateSetting('UseSpecialChars', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.settingItem, styles.settingItemLast]}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.avoidAmbiguousChars')}</ThemedText>
|
||||
<Switch
|
||||
value={settings.UseNonAmbiguousChars}
|
||||
onValueChange={(value) => updateSetting('UseNonAmbiguousChars', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, Platform } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
import 'react-native-get-random-values';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||
import { install } from 'react-native-quick-crypto';
|
||||
|
||||
import { useColors, useColorScheme } from '@/hooks/useColorScheme';
|
||||
@@ -184,7 +186,11 @@ export default function RootLayout() : React.ReactNode {
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<ClipboardCountdownProvider>
|
||||
<RootLayoutNav />
|
||||
<KeyboardProvider>
|
||||
<GestureHandlerRootView>
|
||||
<RootLayoutNav />
|
||||
</GestureHandlerRootView>
|
||||
</KeyboardProvider>
|
||||
</ClipboardCountdownProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function Initialize() : React.ReactNode {
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
// Show modal with error message
|
||||
Alert.alert(t('app.alerts.error'), error);
|
||||
Alert.alert(t('common.error'), error);
|
||||
|
||||
// The logout user and navigate to the login screen.
|
||||
await webApi.logout(error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
|
||||
import { StyleSheet, View, Text, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||
import { StyleSheet, View, Text, TextInput, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
@@ -167,7 +168,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
{DEFAULT_OPTIONS.map(option => (
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.optionButton,
|
||||
@@ -181,7 +182,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
))}
|
||||
|
||||
{selectedOption === 'custom' && (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useFocusEffect } from '@react-navigation/native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions, Alert } from 'react-native';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions, Alert } from 'react-native';
|
||||
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import ConversionUtility from '@/utils/ConversionUtility';
|
||||
@@ -24,6 +24,7 @@ import Logo from '@/assets/images/logo.svg';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { InAppBrowserView } from '@/components/ui/InAppBrowserView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
@@ -63,6 +64,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
|
||||
const [loginStatus, setLoginStatus] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
@@ -583,7 +585,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleTwoFactorSubmit}
|
||||
disabled={isLoading}
|
||||
@@ -593,8 +595,8 @@ export default function LoginScreen() : React.ReactNode {
|
||||
) : (
|
||||
<Text style={styles.buttonText}>{t('auth.verify')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</RobustPressable>
|
||||
<RobustPressable
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={() => {
|
||||
setCredentials({ username: '', password: '' });
|
||||
@@ -607,7 +609,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>{t('common.cancel')}</Text>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
<Text style={styles.textMuted}>
|
||||
{t('auth.authCodeNote')}
|
||||
@@ -619,7 +621,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
<View style={styles.inputContainer}>
|
||||
<MaterialIcons
|
||||
name="person"
|
||||
size={24}
|
||||
size={20}
|
||||
color={colors.textMuted}
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
@@ -639,7 +641,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
<View style={styles.inputContainer}>
|
||||
<MaterialIcons
|
||||
name="lock"
|
||||
size={24}
|
||||
size={20}
|
||||
color={colors.textMuted}
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
@@ -648,15 +650,25 @@ export default function LoginScreen() : React.ReactNode {
|
||||
value={credentials.password}
|
||||
onChangeText={(text) => setCredentials({ ...credentials, password: text })}
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
secureTextEntry
|
||||
secureTextEntry={!showPassword}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
<RobustPressable
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={styles.inputIcon}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={showPassword ? "visibility" : "visibility-off"}
|
||||
size={24}
|
||||
color={colors.textMuted}
|
||||
/>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isLoading}
|
||||
@@ -666,7 +678,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
) : (
|
||||
<Text style={styles.buttonText}>{t('auth.login')}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
<View style={styles.createNewVaultContainer}>
|
||||
<Text style={styles.textMuted}>{t('auth.noAccountYet')} </Text>
|
||||
<InAppBrowserView
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { StyleSheet, View, TextInput, TouchableOpacity, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text } from 'react-native';
|
||||
import { StyleSheet, View, TextInput, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text, Pressable } from 'react-native';
|
||||
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
@@ -14,6 +16,7 @@ import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
@@ -31,6 +34,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const webApi = useWebApi();
|
||||
const [biometricDisplayName, setBiometricDisplayName] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
/**
|
||||
* Check if the key derivation parameters are stored in native storage.
|
||||
@@ -109,8 +113,9 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
} else {
|
||||
Alert.alert(t('common.error'), t('auth.errors.incorrectPassword'));
|
||||
}
|
||||
} catch {
|
||||
Alert.alert(t('common.error'), t('auth.errors.incorrectPasswordFallback'));
|
||||
} catch (error) {
|
||||
console.error('Unlock error:', error);
|
||||
Alert.alert(t('common.error'), t('auth.errors.incorrectPassword'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -297,7 +302,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
<View style={styles.inputContainer}>
|
||||
<MaterialIcons
|
||||
name="lock"
|
||||
size={24}
|
||||
size={20}
|
||||
color={colors.textMuted}
|
||||
style={styles.inputIcon}
|
||||
/>
|
||||
@@ -305,7 +310,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
style={styles.input}
|
||||
placeholder={t('auth.enterPasswordPlaceholder')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
secureTextEntry
|
||||
secureTextEntry={!showPassword}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
autoCapitalize="none"
|
||||
@@ -313,9 +318,19 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={styles.inputIcon}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={showPassword ? "visibility" : "visibility-off"}
|
||||
size={20}
|
||||
color={colors.textMuted}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.button}
|
||||
onPress={handleUnlock}
|
||||
disabled={isLoading}
|
||||
@@ -323,24 +338,24 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isLoading ? t('auth.unlocking') : t('auth.unlock')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
|
||||
{isBiometricsAvailable && (
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.faceIdButton}
|
||||
onPress={handleBiometricsRetry}
|
||||
>
|
||||
<ThemedText style={styles.faceIdButtonText}>{t('auth.tryBiometricAgain', { biometric: biometricDisplayName })}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.logoutButton}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
<ThemedText style={styles.logoutButtonText}>{t('auth.logout')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text } from 'react-native';
|
||||
import { StyleSheet, View, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text } from 'react-native';
|
||||
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
@@ -16,6 +16,7 @@ import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
@@ -67,7 +68,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
*/
|
||||
const handleUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
Alert.alert(t('upgrade.alerts.error'), t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
Alert.alert(t('common.error'), t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
t('upgrade.alerts.selfHostedServer'),
|
||||
t('upgrade.alerts.selfHostedWarning'),
|
||||
[
|
||||
{ text: t('upgrade.alerts.cancel'), style: 'cancel' },
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: t('upgrade.alerts.continueUpgrade'),
|
||||
style: 'default',
|
||||
@@ -100,7 +101,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
*/
|
||||
const performUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
Alert.alert(t('upgrade.alerts.error'), t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
Alert.alert(t('common.error'), t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,7 +229,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
t('upgrade.whatsNew'),
|
||||
`${t('upgrade.whatsNewDescription')}\n\n${latestVersion?.description ?? t('upgrade.noDescriptionAvailable')}`,
|
||||
[
|
||||
{ text: t('upgrade.okay'), style: 'default' }
|
||||
{ text: t('common.ok'), style: 'default' }
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -417,12 +418,12 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
<View style={styles.versionContainer}>
|
||||
<View style={styles.versionHeader}>
|
||||
<ThemedText style={styles.versionTitle}>{t('upgrade.versionInformation')}</ThemedText>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.helpButton}
|
||||
onPress={showVersionDialog}
|
||||
>
|
||||
<ThemedText style={styles.helpButtonText}>?</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
<View style={styles.versionRow}>
|
||||
<ThemedText style={styles.versionLabel}>{t('upgrade.yourVault')}</ThemedText>
|
||||
@@ -438,7 +439,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.button}
|
||||
onPress={handleUpgrade}
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
@@ -446,14 +447,14 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || t('upgrade.upgrading')) : t('upgrade.upgrade')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.logoutButton}
|
||||
onPress={handleLogout}
|
||||
>
|
||||
<ThemedText style={styles.logoutButtonText}>{t('upgrade.logout')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import type { MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
@@ -9,6 +9,7 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { IconSymbolName } from '@/components/ui/IconSymbolName';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
@@ -133,10 +134,9 @@ export function EmailCard({ email }: EmailCardProps) : React.ReactNode {
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.emailCard}
|
||||
onPress={() => router.push(`/(tabs)/emails/${email.id}`)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.emailHeader}>
|
||||
<ThemedText style={styles.emailSubject} numberOfLines={1}>
|
||||
@@ -157,6 +157,6 @@ export function EmailCard({ email }: EmailCardProps) : React.ReactNode {
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
/**
|
||||
@@ -16,6 +18,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||
export function OfflineBanner(): React.ReactNode {
|
||||
const { isOffline } = useAuth();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
if (!isOffline) {
|
||||
@@ -41,7 +44,7 @@ export function OfflineBanner(): React.ReactNode {
|
||||
onSuccess: () => {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Back online',
|
||||
text1: t('app.offline.backOnline'),
|
||||
position: 'bottom'
|
||||
});
|
||||
},
|
||||
@@ -51,7 +54,7 @@ export function OfflineBanner(): React.ReactNode {
|
||||
onOffline: () => {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Still offline',
|
||||
text1: t('app.offline.stillOffline'),
|
||||
position: 'bottom'
|
||||
});
|
||||
},
|
||||
@@ -62,7 +65,7 @@ export function OfflineBanner(): React.ReactNode {
|
||||
onError: (error: string) => {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Still offline',
|
||||
text1: t('app.offline.stillOffline'),
|
||||
text2: error,
|
||||
position: 'bottom'
|
||||
});
|
||||
@@ -98,14 +101,14 @@ export function OfflineBanner(): React.ReactNode {
|
||||
<ThemedView style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<ThemedText style={styles.text}>
|
||||
Offline mode (read-only)
|
||||
{t('app.offline.banner')}
|
||||
</ThemedText>
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={styles.retryButton}
|
||||
onPress={handleRetry}
|
||||
>
|
||||
<Ionicons name="refresh" size={20} color={colors.primarySurfaceText} />
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
|
||||
type NotesSectionProps = {
|
||||
credential: Credential;
|
||||
@@ -108,11 +109,11 @@ export const NotesSection: React.FC<NotesSectionProps> = ({ credential }) : Reac
|
||||
{parts.map((part, index) => {
|
||||
if (part.type === 'url') {
|
||||
return (
|
||||
<Pressable key={index} onPress={() => handleLinkPress(part.url!)}>
|
||||
<RobustPressable key={index} onPress={() => handleLinkPress(part.url!)}>
|
||||
<Text style={styles.link} selectable={true}>
|
||||
{part.content}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</RobustPressable>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { Controller, Control, FieldValues, Path } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, TouchableHighlight, Platform, Modal, ScrollView, Switch, TouchableOpacity } from 'react-native';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, TouchableOpacity, Platform, Modal, ScrollView, Switch } from 'react-native';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
@@ -11,6 +11,7 @@ import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator'
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
export type AdvancedPasswordFieldRef = {
|
||||
focus: () => void;
|
||||
@@ -22,21 +23,16 @@ type AdvancedPasswordFieldProps<T extends FieldValues> = Omit<TextInputProps, 'v
|
||||
name: Path<T>;
|
||||
control: Control<T>;
|
||||
required?: boolean;
|
||||
initialSettings: PasswordSettings;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
isNewCredential?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced password field component with inline length slider and settings modal.
|
||||
*/
|
||||
const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, AdvancedPasswordFieldProps<FieldValues>>(({
|
||||
label,
|
||||
name,
|
||||
control,
|
||||
required,
|
||||
initialSettings,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
isNewCredential = false,
|
||||
@@ -44,24 +40,21 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
}, ref) => {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const inputRef = React.useRef<TextInput>(null);
|
||||
const currentValue = useRef<string>('');
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings | null>(null);
|
||||
const [previewPassword, setPreviewPassword] = useState<string>('');
|
||||
const [displayLength, setDisplayLength] = useState<number>(initialSettings.Length);
|
||||
const [hasAutoGenerated, setHasAutoGenerated] = useState(false);
|
||||
const onChangeRef = useRef<((value: string) => void) | null>(null);
|
||||
|
||||
// Use controlled or uncontrolled showPassword state
|
||||
const [sliderValue, setSliderValue] = useState<number>(16); // Default until loaded from DB
|
||||
const fieldOnChangeRef = useRef<((value: string) => void) | null>(null);
|
||||
const lastGeneratedLength = useRef<number>(0);
|
||||
const isSliding = useRef(false);
|
||||
const hasSetInitialLength = useRef(false);
|
||||
const currentPasswordRef = useRef<string>('');
|
||||
const dbContext = useDb();
|
||||
const showPassword = controlledShowPassword ?? internalShowPassword;
|
||||
|
||||
/**
|
||||
* Set the showPassword state.
|
||||
*/
|
||||
const setShowPasswordState = useCallback((show: boolean): void => {
|
||||
const setShowPasswordState = useCallback((show: boolean) => {
|
||||
if (controlledShowPassword !== undefined) {
|
||||
onShowPasswordChange?.(show);
|
||||
} else {
|
||||
@@ -69,159 +62,144 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
}
|
||||
}, [controlledShowPassword, onShowPasswordChange]);
|
||||
|
||||
/**
|
||||
* Expose focus and selectAll methods through ref.
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
/**
|
||||
* Focus the input field.
|
||||
*/
|
||||
focus: (): void => {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
/**
|
||||
* Select all text in the input field.
|
||||
*/
|
||||
selectAll: (): void => {
|
||||
inputRef.current?.setSelection(0, currentValue.current.length);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* Initialize settings when initialSettings change.
|
||||
*/
|
||||
// Load password settings from database
|
||||
useEffect(() => {
|
||||
setCurrentSettings({ ...initialSettings });
|
||||
setDisplayLength(initialSettings.Length);
|
||||
}, [initialSettings]);
|
||||
|
||||
/**
|
||||
* Auto-generate password for new credentials after component mount.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isNewCredential && !hasAutoGenerated && currentSettings.Length > 0 && onChangeRef.current) {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(currentSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
onChangeRef.current(password);
|
||||
setShowPasswordState(true);
|
||||
setHasAutoGenerated(true);
|
||||
if (dbContext.sqliteClient) {
|
||||
const settings = await dbContext.sqliteClient.getPasswordSettings();
|
||||
setCurrentSettings(settings);
|
||||
// Only set slider value from settings if we don't have a password value yet
|
||||
if (!hasSetInitialLength.current && isNewCredential) {
|
||||
setSliderValue(settings.Length);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error auto-generating password:', error);
|
||||
setHasAutoGenerated(true);
|
||||
console.error('Error loading password settings:', error);
|
||||
}
|
||||
};
|
||||
loadSettings();
|
||||
}, [dbContext.sqliteClient, isNewCredential]);
|
||||
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current?.focus(),
|
||||
selectAll: () => {
|
||||
const input = inputRef.current;
|
||||
if (input && input.props.value) {
|
||||
input.setSelection(0, String(input.props.value).length);
|
||||
}
|
||||
}
|
||||
}, [isNewCredential, hasAutoGenerated, currentSettings, setShowPasswordState]);
|
||||
}), []);
|
||||
|
||||
/**
|
||||
* Generate a password with the given settings.
|
||||
*/
|
||||
const generatePassword = useCallback((settings: PasswordSettings, onChange: (value: string) => void) => {
|
||||
const generatePassword = useCallback((settings: PasswordSettings): string => {
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
onChange(password);
|
||||
setShowPasswordState(true);
|
||||
return password;
|
||||
return passwordGenerator.generateRandomPassword();
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
return '';
|
||||
}
|
||||
}, [setShowPasswordState]);
|
||||
|
||||
/**
|
||||
* Generate a preview password for the settings modal.
|
||||
*/
|
||||
const generatePreview = useCallback((settings: PasswordSettings) => {
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating preview password:', error);
|
||||
setPreviewPassword('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Individual handlers for each switch to prevent re-renders.
|
||||
*/
|
||||
const handleLowercaseChange = useCallback((value: boolean) => {
|
||||
const newSettings = { ...currentSettings, UseLowercase: value };
|
||||
setCurrentSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
const handleGeneratePassword = useCallback(() => {
|
||||
if (fieldOnChangeRef.current && currentSettings) {
|
||||
const password = generatePassword(currentSettings);
|
||||
if (password) {
|
||||
fieldOnChangeRef.current(password);
|
||||
setShowPasswordState(true);
|
||||
}
|
||||
}
|
||||
}, [currentSettings, generatePassword, setShowPasswordState]);
|
||||
|
||||
const handleUppercaseChange = useCallback((value: boolean) => {
|
||||
const newSettings = { ...currentSettings, UseUppercase: value };
|
||||
setCurrentSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
const handleSliderChange = useCallback((value: number) => {
|
||||
const roundedLength = Math.round(value);
|
||||
setSliderValue(roundedLength);
|
||||
|
||||
const handleNumbersChange = useCallback((value: boolean) => {
|
||||
const newSettings = { ...currentSettings, UseNumbers: value };
|
||||
setCurrentSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
// Only generate if value actually changed and we're actively sliding
|
||||
if (roundedLength !== lastGeneratedLength.current && isSliding.current) {
|
||||
lastGeneratedLength.current = roundedLength;
|
||||
|
||||
const handleSpecialCharsChange = useCallback((value: boolean) => {
|
||||
const newSettings = { ...currentSettings, UseSpecialChars: value };
|
||||
setCurrentSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
// Show password when sliding
|
||||
if (!showPassword) {
|
||||
setShowPasswordState(true);
|
||||
}
|
||||
|
||||
const handleNonAmbiguousChange = useCallback((value: boolean) => {
|
||||
const newSettings = { ...currentSettings, UseNonAmbiguousChars: value };
|
||||
setCurrentSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
const newSettings = { ...(currentSettings || {}), Length: roundedLength } as PasswordSettings;
|
||||
if (fieldOnChangeRef.current && currentSettings) {
|
||||
const password = generatePassword(newSettings);
|
||||
if (password) {
|
||||
fieldOnChangeRef.current(password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentSettings, generatePassword, showPassword, setShowPasswordState]);
|
||||
|
||||
/**
|
||||
* Handle opening the settings modal.
|
||||
*/
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
// Focus the password input field to prevent scroll jumping
|
||||
inputRef.current?.focus();
|
||||
// Generate initial preview when modal opens
|
||||
generatePreview(currentSettings);
|
||||
setShowSettingsModal(true);
|
||||
}, [currentSettings, generatePreview]);
|
||||
const handleSliderStart = useCallback(() => {
|
||||
isSliding.current = true;
|
||||
// Initialize lastGeneratedLength when starting to slide
|
||||
lastGeneratedLength.current = sliderValue;
|
||||
}, [sliderValue]);
|
||||
|
||||
const handleSliderComplete = useCallback((value: number) => {
|
||||
isSliding.current = false;
|
||||
const roundedLength = Math.round(value);
|
||||
if (currentSettings) {
|
||||
const newSettings = { ...currentSettings, Length: roundedLength };
|
||||
setCurrentSettings(newSettings);
|
||||
}
|
||||
lastGeneratedLength.current = 0; // Reset for next sliding session
|
||||
}, [currentSettings]);
|
||||
|
||||
/**
|
||||
* Handle refreshing the preview password in the modal.
|
||||
*/
|
||||
const handleRefreshPreview = useCallback(() => {
|
||||
generatePreview(currentSettings);
|
||||
}, [currentSettings, generatePreview]);
|
||||
if (currentSettings) {
|
||||
const password = generatePassword(currentSettings);
|
||||
setPreviewPassword(password);
|
||||
}
|
||||
}, [currentSettings, generatePassword]);
|
||||
|
||||
/**
|
||||
* Handle using the generated password from the modal.
|
||||
*/
|
||||
const handleUsePassword = useCallback((onChange: (value: string) => void) => {
|
||||
onChange(previewPassword);
|
||||
setShowPasswordState(true);
|
||||
setShowSettingsModal(false);
|
||||
const handleUsePassword = useCallback(() => {
|
||||
if (fieldOnChangeRef.current && previewPassword) {
|
||||
fieldOnChangeRef.current(previewPassword);
|
||||
setShowPasswordState(true);
|
||||
setShowSettingsModal(false);
|
||||
}
|
||||
}, [previewPassword, setShowPasswordState]);
|
||||
|
||||
const colorRed = 'red';
|
||||
const modalBackgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
if (currentSettings) {
|
||||
const password = generatePassword(currentSettings);
|
||||
setPreviewPassword(password);
|
||||
setShowSettingsModal(true);
|
||||
}
|
||||
}, [currentSettings, generatePassword]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const updateSetting = useCallback((key: keyof PasswordSettings, value: boolean) => {
|
||||
setCurrentSettings(prev => {
|
||||
if (!prev) return prev;
|
||||
const newSettings = { ...prev, [key]: value };
|
||||
const password = generatePassword(newSettings);
|
||||
setPreviewPassword(password);
|
||||
return newSettings;
|
||||
});
|
||||
}, [generatePassword]);
|
||||
|
||||
const styles = useMemo(() => StyleSheet.create({
|
||||
button: {
|
||||
borderLeftColor: colors.accentBorder,
|
||||
borderLeftWidth: 1,
|
||||
padding: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
clearButton: {
|
||||
borderRadius: 6,
|
||||
marginRight: 4,
|
||||
padding: 6,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
padding: 8,
|
||||
},
|
||||
errorText: {
|
||||
color: colorRed,
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
@@ -229,8 +207,8 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
marginRight: 5,
|
||||
padding: 10,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
inputContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -241,7 +219,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
flexDirection: 'row',
|
||||
},
|
||||
inputError: {
|
||||
borderColor: colorRed,
|
||||
borderColor: 'red',
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: 6,
|
||||
@@ -267,7 +245,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
},
|
||||
modalOverlay: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: modalBackgroundColor,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -301,18 +279,19 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
padding: 10,
|
||||
},
|
||||
requiredIndicator: {
|
||||
color: colorRed,
|
||||
color: 'red',
|
||||
marginLeft: 4,
|
||||
},
|
||||
settingItem: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
settingLabel: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
},
|
||||
settingsButton: {
|
||||
marginLeft: 8,
|
||||
@@ -323,19 +302,17 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
},
|
||||
slider: {
|
||||
height: 40,
|
||||
paddingVertical: 10, // Add padding for better touch area on Android
|
||||
width: '100%',
|
||||
},
|
||||
sliderContainer: {
|
||||
marginTop: 8,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 5, // Additional padding around the slider
|
||||
},
|
||||
sliderHeader: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
sliderLabel: {
|
||||
color: colors.textMuted,
|
||||
@@ -354,13 +331,10 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
useButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.accentBorder,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
color: colors.primarySurfaceText,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: 6,
|
||||
padding: 12,
|
||||
},
|
||||
useButtonText: {
|
||||
color: colors.text,
|
||||
@@ -368,150 +342,32 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle closing the settings modal.
|
||||
*/
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setShowSettingsModal(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Render the settings modal.
|
||||
*/
|
||||
const renderSettingsModal = useCallback((onChange: (value: string) => void) => {
|
||||
if (!showSettingsModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={showSettingsModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleCloseModal}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<ThemedText style={styles.modalTitle}>{t('credentials.changePasswordComplexity')}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={handleCloseModal}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.previewContainer}>
|
||||
<View style={styles.previewInputContainer}>
|
||||
<TextInput
|
||||
style={styles.previewInput}
|
||||
value={previewPassword}
|
||||
editable={false}
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.refreshButton}
|
||||
onPress={handleRefreshPreview}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingsSection}>
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeLowercase')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings.UseLowercase}
|
||||
onValueChange={handleLowercaseChange}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeUppercase')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings.UseUppercase}
|
||||
onValueChange={handleUppercaseChange}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeNumbers')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings.UseNumbers}
|
||||
onValueChange={handleNumbersChange}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeSpecialChars')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings.UseSpecialChars}
|
||||
onValueChange={handleSpecialCharsChange}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.avoidAmbiguousChars')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings.UseNonAmbiguousChars}
|
||||
onValueChange={handleNonAmbiguousChange}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.useButton}
|
||||
onPress={() => handleUsePassword(onChange)}
|
||||
>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={colors.text} />
|
||||
<ThemedText style={styles.useButtonText}>{t('common.use')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}, [
|
||||
showSettingsModal,
|
||||
styles,
|
||||
previewPassword,
|
||||
handleRefreshPreview,
|
||||
currentSettings,
|
||||
handleLowercaseChange,
|
||||
handleUppercaseChange,
|
||||
handleNumbersChange,
|
||||
handleSpecialCharsChange,
|
||||
handleNonAmbiguousChange,
|
||||
colors,
|
||||
handleUsePassword,
|
||||
handleCloseModal,
|
||||
t
|
||||
]);
|
||||
}), [colors]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
currentValue.current = value as string;
|
||||
onChangeRef.current = onChange; // Assign onChange to the ref
|
||||
const showClearButton = Platform.OS === 'android' && value && value.length > 0 && isFocused;
|
||||
fieldOnChangeRef.current = onChange;
|
||||
currentPasswordRef.current = value as string || '';
|
||||
|
||||
// Use useEffect to update slider value when password value changes
|
||||
// This avoids setState during render
|
||||
useEffect(() => {
|
||||
if (!hasSetInitialLength.current) {
|
||||
if (!isNewCredential && value && typeof value === 'string' && value.length > 0) {
|
||||
// Editing existing credential: use actual password length
|
||||
setSliderValue(value.length);
|
||||
hasSetInitialLength.current = true;
|
||||
} else if (isNewCredential) {
|
||||
// New credential: settings default is already set
|
||||
hasSetInitialLength.current = true;
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const showClearButton = Platform.OS === 'android' && value && value.length > 0;
|
||||
|
||||
return (
|
||||
<View style={styles.inputGroup}>
|
||||
@@ -530,54 +386,50 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
clearButtonMode={Platform.OS === 'ios' ? "while-editing" : "never"}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
secureTextEntry={!showPassword}
|
||||
multiline={false}
|
||||
numberOfLines={1}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{showClearButton && (
|
||||
<TouchableHighlight
|
||||
<TouchableOpacity
|
||||
style={styles.clearButton}
|
||||
onPress={() => onChange('')}
|
||||
underlayColor={colors.accentBackground}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="close" size={16} color={colors.textMuted} />
|
||||
</TouchableHighlight>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableHighlight
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => setShowPasswordState(!showPassword)}
|
||||
underlayColor={colors.accentBackground}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={showPassword ? "visibility-off" : "visibility"}
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableHighlight
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => generatePassword(currentSettings, onChange)}
|
||||
underlayColor={colors.accentBackground}
|
||||
onPress={handleGeneratePassword}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={colors.primary} />
|
||||
</TouchableHighlight>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Inline Password Length Slider */}
|
||||
<View style={styles.sliderContainer}>
|
||||
<View style={styles.sliderHeader}>
|
||||
<ThemedText style={styles.sliderLabel}>{t('credentials.passwordLength')}</ThemedText>
|
||||
<View style={styles.sliderValueContainer}>
|
||||
<ThemedText style={styles.sliderValue}>{displayLength}</ThemedText>
|
||||
<ThemedText style={styles.sliderValue}>{sliderValue}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={styles.settingsButton}
|
||||
onPress={handleOpenSettings}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
@@ -588,19 +440,10 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
style={styles.slider}
|
||||
minimumValue={8}
|
||||
maximumValue={64}
|
||||
value={currentSettings.Length}
|
||||
onValueChange={(value) => {
|
||||
const roundedLength = Math.round(value);
|
||||
setDisplayLength(roundedLength); // Update display immediately
|
||||
const tempSettings = { ...currentSettings, Length: roundedLength };
|
||||
generatePassword(tempSettings, onChange);
|
||||
}}
|
||||
onSlidingComplete={(value) => {
|
||||
const roundedLength = Math.round(value);
|
||||
const newSettings = { ...currentSettings, Length: roundedLength };
|
||||
setCurrentSettings(newSettings);
|
||||
setDisplayLength(roundedLength); // Ensure display matches final value
|
||||
}}
|
||||
value={sliderValue}
|
||||
onValueChange={handleSliderChange}
|
||||
onSlidingStart={handleSliderStart}
|
||||
onSlidingComplete={handleSliderComplete}
|
||||
step={1}
|
||||
minimumTrackTintColor={colors.primary}
|
||||
maximumTrackTintColor={colors.accentBorder}
|
||||
@@ -610,7 +453,107 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
|
||||
{error && <ThemedText style={styles.errorText}>{error.message}</ThemedText>}
|
||||
|
||||
{renderSettingsModal(onChange)}
|
||||
<Modal
|
||||
visible={showSettingsModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowSettingsModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<ThemedText style={styles.modalTitle}>{t('credentials.changePasswordComplexity')}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setShowSettingsModal(false)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.previewContainer}>
|
||||
<View style={styles.previewInputContainer}>
|
||||
<TextInput
|
||||
style={styles.previewInput}
|
||||
value={previewPassword}
|
||||
editable={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.refreshButton}
|
||||
onPress={handleRefreshPreview}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingsSection}>
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeLowercase')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings?.UseLowercase ?? true}
|
||||
onValueChange={(value) => updateSetting('UseLowercase', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeUppercase')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings?.UseUppercase ?? true}
|
||||
onValueChange={(value) => updateSetting('UseUppercase', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeNumbers')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings?.UseNumbers ?? true}
|
||||
onValueChange={(value) => updateSetting('UseNumbers', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.includeSpecialChars')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings?.UseSpecialChars ?? true}
|
||||
onValueChange={(value) => updateSetting('UseSpecialChars', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<ThemedText style={styles.settingLabel}>{t('credentials.avoidAmbiguousChars')}</ThemedText>
|
||||
<Switch
|
||||
value={currentSettings?.UseNonAmbiguousChars ?? false}
|
||||
onValueChange={(value) => updateSetting('UseNonAmbiguousChars', value)}
|
||||
trackColor={{ false: colors.accentBorder, true: colors.primary }}
|
||||
thumbColor={Platform.OS === 'android' ? colors.background : undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.useButton}
|
||||
onPress={handleUsePassword}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={colors.text} />
|
||||
<ThemedText style={styles.useButtonText}>{t('common.use')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -44,6 +44,9 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
}, [animatedWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
let animationRef: Animated.CompositeAnimation | null = null;
|
||||
let isCancelled = false;
|
||||
|
||||
/* Handle animation based on whether this field is active */
|
||||
if (isCountingDown) {
|
||||
// This field is now active - reset and start animation
|
||||
@@ -52,15 +55,22 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
|
||||
// Get timeout and start animation
|
||||
getClipboardClearTimeout().then((timeoutSeconds) => {
|
||||
if (timeoutSeconds > 0 && activeFieldId === fieldId) {
|
||||
Animated.timing(animatedWidth, {
|
||||
if (!isCancelled && timeoutSeconds > 0 && activeFieldId === fieldId) {
|
||||
animationRef = Animated.timing(animatedWidth, {
|
||||
toValue: 0,
|
||||
duration: timeoutSeconds * 1000,
|
||||
useNativeDriver: false,
|
||||
easing: Easing.linear,
|
||||
}).start((finished) => {
|
||||
if (finished && activeFieldId === fieldId) {
|
||||
setActiveField(null);
|
||||
});
|
||||
|
||||
animationRef.start((finished) => {
|
||||
if (!isCancelled && finished && activeFieldId === fieldId) {
|
||||
// Use requestAnimationFrame to defer state update
|
||||
requestAnimationFrame(() => {
|
||||
if (!isCancelled) {
|
||||
setActiveField(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -70,6 +80,15 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
animatedWidth.stopAnimation();
|
||||
animatedWidth.setValue(0);
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
if (animationRef) {
|
||||
animationRef.stop();
|
||||
}
|
||||
animatedWidth.stopAnimation();
|
||||
};
|
||||
}, [isCountingDown, activeFieldId, fieldId, animatedWidth, setActiveField, getClipboardClearTimeout]);
|
||||
|
||||
/**
|
||||
@@ -86,16 +105,12 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
|
||||
// Handle animation state
|
||||
if (timeoutSeconds > 0) {
|
||||
// Clear any existing active field first (this will cancel its animation)
|
||||
setActiveField(null);
|
||||
|
||||
/*
|
||||
* Now set this field as active - animation will be handled by the effect
|
||||
* Use setTimeout to ensure state update happens in next tick
|
||||
*/
|
||||
setTimeout(() => {
|
||||
setActiveField(fieldId);
|
||||
}, 0);
|
||||
// Clear any existing active field and set this one as active
|
||||
// Use functional update to avoid closure issues
|
||||
setActiveField(() => {
|
||||
// If there was a previous field, its animation will be stopped by the effect
|
||||
return fieldId;
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.OS !== 'android') {
|
||||
@@ -120,7 +135,7 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
};
|
||||
|
||||
const displayValue = type === 'password' && !isPasswordVisible
|
||||
? '••••••••'
|
||||
? '•'.repeat(value?.length || 0)
|
||||
: value;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
|
||||
import { StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
|
||||
type ThemedButtonProps = {
|
||||
title: string;
|
||||
@@ -50,7 +51,7 @@ export const ThemedButton: React.FC<ThemedButtonProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<RobustPressable
|
||||
style={[
|
||||
styles.button,
|
||||
disabled && styles.buttonDisabled,
|
||||
@@ -68,6 +69,6 @@ export const ThemedButton: React.FC<ThemedButtonProps> = ({
|
||||
color={colors.background}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</RobustPressable>
|
||||
);
|
||||
};
|
||||
43
apps/mobile-app/components/ui/RobustPressable.tsx
Normal file
43
apps/mobile-app/components/ui/RobustPressable.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { StyleProp, ViewStyle, Platform } from 'react-native';
|
||||
import { Pressable } from 'react-native-gesture-handler';
|
||||
|
||||
interface IRobustPressableProps {
|
||||
onPress?: () => void;
|
||||
children: React.ReactNode;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
disabled?: boolean;
|
||||
pressRetentionOffset?: number;
|
||||
hitSlop?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simplified robust Pressable component that uses react-native-gesture-handler
|
||||
* for better performance and reliability, especially with Magic Keyboard trackpad
|
||||
* interactions on iPad. Simulates TouchableOpacity behavior with opacity feedback.
|
||||
* Only exposes essential props while handling all press behavior internally.
|
||||
*/
|
||||
export const RobustPressable: React.FC<IRobustPressableProps> = ({
|
||||
onPress,
|
||||
children,
|
||||
style,
|
||||
disabled,
|
||||
pressRetentionOffset = 10,
|
||||
hitSlop = 10,
|
||||
}) => {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
hitSlop={hitSlop}
|
||||
pressRetentionOffset={pressRetentionOffset}
|
||||
disabled={disabled}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
style={({ pressed }) => [
|
||||
style,
|
||||
{ opacity: pressed ? 0.6 : 1 },
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { NavigationContainerRef, ParamListBase } from '@react-navigation/native';
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
|
||||
180
apps/mobile-app/eslint.config.mjs
Normal file
180
apps/mobile-app/eslint.config.mjs
Normal file
@@ -0,0 +1,180 @@
|
||||
import js from "@eslint/js";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import jsdocPlugin from "eslint-plugin-jsdoc";
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"eslint.config.js",
|
||||
"**/metro.config.js",
|
||||
"**/dist/**",
|
||||
"**/ios/**",
|
||||
"**/android/**",
|
||||
"**/coverage/**",
|
||||
"node_modules/**",
|
||||
"src/utils/dist/**",
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["app/**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin,
|
||||
"react": reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
"import": importPlugin,
|
||||
"jsdoc": jsdocPlugin,
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...reactPlugin.configs.recommended.rules,
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
"curly": ["error", "all"],
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/no-unused-prop-types": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {
|
||||
"vars": "all",
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": true,
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
"VariableDeclarator": 1,
|
||||
"outerIIFEBody": 1,
|
||||
"MemberExpression": 1,
|
||||
"FunctionDeclaration": { "parameters": 1, "body": 1 },
|
||||
"FunctionExpression": { "parameters": 1, "body": 1 },
|
||||
"CallExpression": { "arguments": 1 },
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1,
|
||||
"ImportDeclaration": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoreComments": false
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1, "maxBOF": 0 }],
|
||||
"no-console": ["error", { allow: ["warn", "error", "info", "debug"] }],
|
||||
"jsdoc/require-jsdoc": ["error", {
|
||||
"require": {
|
||||
"FunctionDeclaration": true,
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true,
|
||||
"ArrowFunctionExpression": true,
|
||||
"FunctionExpression": true
|
||||
}
|
||||
}],
|
||||
"jsdoc/require-description": ["error", {
|
||||
"contexts": [
|
||||
"FunctionDeclaration",
|
||||
"MethodDefinition",
|
||||
"ClassDeclaration",
|
||||
"ArrowFunctionExpression",
|
||||
"FunctionExpression"
|
||||
]
|
||||
}],
|
||||
"spaced-comment": ["error", "always"],
|
||||
"multiline-comment-style": ["error", "starred-block"],
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": ["error"],
|
||||
"@typescript-eslint/typedef": ["error"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"prefix": ["I"]
|
||||
},
|
||||
{
|
||||
"selector": "class",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/jsx-no-constructed-context-values": "error",
|
||||
"import/no-unresolved": [
|
||||
"error",
|
||||
{
|
||||
ignore: ['^#imports$'] // Ignore virtual imports from WXT which are not resolved by the typescript compiler
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"builtin", // Node "fs", "path", etc.
|
||||
"external", // "react", "lodash", etc.
|
||||
"internal", // Aliased paths like "@/utils"
|
||||
"parent", // "../"
|
||||
"sibling", // "./"
|
||||
"index", // "./index"
|
||||
"object", // import 'foo'
|
||||
"type" // import type ...
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
pattern: "@/entrypoints/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/utils/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/hooks/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
order: "asc",
|
||||
caseInsensitive: true
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
NodeJS: true,
|
||||
chrome: 'readonly',
|
||||
React: 'readonly',
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Toast from 'react-native-toast-message';
|
||||
@@ -316,10 +318,10 @@ export function useVaultMutate() : {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('vault.errors.operationFailed'),
|
||||
text2: error instanceof Error ? error.message : t('vault.errors.unknownError'),
|
||||
text2: error instanceof Error ? error.message : t('common.unknownError'),
|
||||
position: 'bottom'
|
||||
});
|
||||
options.onError?.(error instanceof Error ? error : new Error(t('vault.errors.unknownError')));
|
||||
options.onError?.(error instanceof Error ? error : new Error(t('common.unknownError')));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setSyncStatus('');
|
||||
@@ -397,10 +399,10 @@ export function useVaultMutate() : {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('vault.errors.operationFailed'),
|
||||
text2: error instanceof Error ? error.message : t('vault.errors.unknownError'),
|
||||
text2: error instanceof Error ? error.message : t('common.unknownError'),
|
||||
position: 'bottom'
|
||||
});
|
||||
options.onError?.(error instanceof Error ? error : new Error(t('vault.errors.unknownError')));
|
||||
options.onError?.(error instanceof Error ? error : new Error(t('common.unknownError')));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setSyncStatus('');
|
||||
|
||||
@@ -154,7 +154,7 @@ export const useVaultSync = () : {
|
||||
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : t('vault.errors.unknownErrorDuringSync');
|
||||
const errorMessage = err instanceof Error ? err.message : t('common.unknownError');
|
||||
console.error('Vault sync error:', err);
|
||||
|
||||
// Check if it's a network error
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"yes": "Yes",
|
||||
@@ -13,7 +14,9 @@
|
||||
"never": "Never",
|
||||
"copied": "Copied to clipboard",
|
||||
"loadMore": "Load more",
|
||||
"use": "Use"
|
||||
"use": "Use",
|
||||
"confirm": "Confirm",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Log in",
|
||||
@@ -47,7 +50,6 @@
|
||||
"serverErrorSelfHosted": "Could not reach the API. For self-hosted instances, please verify the API endpoint is reachable by navigating to it in a browser: it should display 'OK'.",
|
||||
"networkError": "Network request failed. Please check your internet connection and try again.",
|
||||
"networkErrorSelfHosted": "Network request failed. Check your network connection and server availability. For self-hosted instances, please ensure you have a valid SSL certificate installed. Self-signed certificates are not supported on mobile devices for security reasons.",
|
||||
"incorrectPasswordFallback": "Incorrect password. Please try again.",
|
||||
"sessionExpired": "Your session has expired. Please login again.",
|
||||
"tokenRefreshFailed": "Failed to refresh authentication token",
|
||||
"httpError": "HTTP error: {{status}}"
|
||||
@@ -76,11 +78,9 @@
|
||||
"errorDuringPasswordChange": "Error during password change operation. Please log in again to retrieve your latest vault.",
|
||||
"failedToSyncVault": "Failed to sync vault",
|
||||
"operationFailed": "Operation failed",
|
||||
"unknownError": "Unknown error",
|
||||
"versionNotSupported": "This version of the AliasVault mobile app is not supported by the server anymore. Please update your app to the latest version.",
|
||||
"serverNeedsUpdate": "The AliasVault server needs to be updated to a newer version in order to use this mobile app. Please contact support if you need help.",
|
||||
"vaultDecryptFailed": "Vault could not be decrypted, if the problem persists please logout and login again.",
|
||||
"unknownErrorDuringSync": "Unknown error during vault sync",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
}
|
||||
},
|
||||
@@ -162,8 +162,7 @@
|
||||
"errors": {
|
||||
"loadFailed": "Failed to load credential",
|
||||
"generateUsernameFailed": "Failed to generate username",
|
||||
"generatePasswordFailed": "Failed to generate password",
|
||||
"generic": "Error"
|
||||
"generatePasswordFailed": "Failed to generate password"
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Credential Options",
|
||||
@@ -220,6 +219,17 @@
|
||||
"batteryOptimizationHelpDescription": "Android's battery optimization prevents reliable clipboard clearing when the app is in the background. Disabling battery optimization for AliasVault allows precise background clipboard clearing and automatically grants necessary alarm permissions.",
|
||||
"disableBatteryOptimization": "Disable battery optimization",
|
||||
"identityGenerator": "Identity Generator",
|
||||
"passwordGenerator": "Password Generator",
|
||||
"importExport": "Import / Export",
|
||||
"importSectionTitle": "Import",
|
||||
"importSectionDescription": "Import your passwords from other password managers or from a previous AliasVault export.",
|
||||
"importWebNote": "To import credentials from existing password managers, please login to the web app. The import feature is currently only available on the web version.",
|
||||
"exportSectionTitle": "Export",
|
||||
"exportSectionDescription": "Export your vault data to a CSV file. This file can be used as a back-up and can also be imported into other password managers.",
|
||||
"exportCsvButton": "Export vault to CSV file",
|
||||
"exporting": "Exporting...",
|
||||
"exportConfirmTitle": "Export Vault",
|
||||
"exportWarning": "Warning: Exporting your vault to an unencrypted file will expose all of your passwords and sensitive information in plain text. Only do this on trusted devices and ensure you:\n\n• Store the exported file in a secure location\n• Delete the file when you no longer need it\n• Never share the exported file with others\n\nAre you sure you want to continue with the export?",
|
||||
"security": "Security",
|
||||
"appVersion": "App version {{version}} ({{url}})",
|
||||
"autoLockOptions": {
|
||||
@@ -275,6 +285,10 @@
|
||||
"genderUpdateFailed": "Failed to update gender setting."
|
||||
}
|
||||
},
|
||||
"passwordGeneratorSettings": {
|
||||
"description": "Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"securitySettings": {
|
||||
"title": "Security",
|
||||
"description": "Manage your account and vault security settings.",
|
||||
@@ -424,12 +438,16 @@
|
||||
"openingVaultReadOnly": "Opening vault in read-only mode",
|
||||
"retryingConnection": "Retrying connection..."
|
||||
},
|
||||
"offline": {
|
||||
"banner": "Offline mode (read-only)",
|
||||
"backOnline": "Back online",
|
||||
"stillOffline": "Still offline"
|
||||
},
|
||||
"alerts": {
|
||||
"syncIssue": "Sync Issue",
|
||||
"syncIssueMessage": "The AliasVault server could not be reached and your vault could not be synced. Would you like to open your local vault in read-only mode or retry the connection?",
|
||||
"openLocalVault": "Open Local Vault",
|
||||
"retrySync": "Retry Sync",
|
||||
"error": "Error"
|
||||
"retrySync": "Retry Sync"
|
||||
},
|
||||
"navigation": {
|
||||
"login": "Login",
|
||||
@@ -467,7 +485,6 @@
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
@@ -477,11 +494,9 @@
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"delete": "Löschen",
|
||||
"save": "Speichern",
|
||||
"yes": "Ja",
|
||||
@@ -13,7 +14,9 @@
|
||||
"never": "Niemals",
|
||||
"copied": "In die Zwischenablage kopiert",
|
||||
"loadMore": "Mehr laden",
|
||||
"use": "Benutzen"
|
||||
"use": "Benutzen",
|
||||
"confirm": "Bestätigen",
|
||||
"unknownError": "Unbekannter Fehler"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Anmelden",
|
||||
@@ -47,7 +50,6 @@
|
||||
"serverErrorSelfHosted": "Die API konnte nicht erreicht werden. Für selbstgehostete Instanzen überprüfe bitte, ob der API-Endpunkt erreichbar ist, indem Du in einem Browser zu ihm navigieren: Er sollte 'OK' anzeigen.",
|
||||
"networkError": "Netzwerkanfrage fehlgeschlagen. Bitte überprüfe Deine Internetverbindung und versuche es erneut.",
|
||||
"networkErrorSelfHosted": "Netzwerkanfrage fehlgeschlagen. Überprüfe deine Netzwerkverbindung und die Server-Verfügbarkeit. Stelle bei selbstgehosteten Instanzen sicher, dass ein gültiges SSL-Zertifikat installiert ist. Aus Sicherheitsgründen werden selbstsignierte Zertifikate auf mobilen Geräten nicht unterstützt.",
|
||||
"incorrectPasswordFallback": "Falsches Passwort. Bitte versuche es erneut.",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an.",
|
||||
"tokenRefreshFailed": "Aktualisieren des Authentifizierungstokens ist fehlgeschlagen",
|
||||
"httpError": "HTTP-Fehler: {{status}}"
|
||||
@@ -76,11 +78,9 @@
|
||||
"errorDuringPasswordChange": "Fehler beim Ändern des Passworts. Bitte melde Dich erneut an, um Deinen letzten Tresor abzurufen.",
|
||||
"failedToSyncVault": "Fehler beim Synchronisieren des Tresors",
|
||||
"operationFailed": "Vorgang fehlgeschlagen",
|
||||
"unknownError": "Unbekannter Fehler",
|
||||
"versionNotSupported": "Diese Version der AliasVault-App wird vom Server nicht mehr unterstützt. Bitte aktualisiere Deine App auf die neueste Version.",
|
||||
"serverNeedsUpdate": "Der AliasVault-Server muss auf eine neuere Version aktualisiert werden, um diese mobile App nutzen zu können. Bitte kontaktiere den Support, falls Du Hilfe benötigst.",
|
||||
"vaultDecryptFailed": "Tresor konnte nicht entschlüsselt werden. Wenn das Problem weiterhin besteht, melden Dich bitte erneut an.",
|
||||
"unknownErrorDuringSync": "Unbekannter Fehler während der Synchronisation des Tresors",
|
||||
"passwordChanged": "Dein Passwort hat sich seit Deiner letzten Anmeldung geändert. Bitte melden Dich aus Sicherheitsgründen erneut an."
|
||||
}
|
||||
},
|
||||
@@ -162,8 +162,7 @@
|
||||
"errors": {
|
||||
"loadFailed": "Laden des Zugangs fehlgeschlagen",
|
||||
"generateUsernameFailed": "Benutzername konnte nicht generiert werden",
|
||||
"generatePasswordFailed": "Passwort konnte nicht generiert werden",
|
||||
"generic": "Fehler"
|
||||
"generatePasswordFailed": "Passwort konnte nicht generiert werden"
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Zugangsoptionen",
|
||||
@@ -220,6 +219,17 @@
|
||||
"batteryOptimizationHelpDescription": "Die Akkuoptimierung von Android verhindert, dass die Zwischenablage zuverlässig gelöscht wird, wenn die App im Hintergrund ist. Das Deaktivieren der Akkuoptimierung für AliasVault ermöglicht das zuverlässige Löschen der Zwischenablage und gewährt automatisch die notwendigen Alarmberechtigungen.",
|
||||
"disableBatteryOptimization": "Akkuoptimierung deaktivieren",
|
||||
"identityGenerator": "Identitätsgenerator",
|
||||
"passwordGenerator": "Passwortgenerator",
|
||||
"importExport": "Import und Export",
|
||||
"importSectionTitle": "Importieren",
|
||||
"importSectionDescription": "Importiere Deine Passwörter von anderen Passwort-Managern oder von einem früheren AliasVault-Export.",
|
||||
"importWebNote": "Um Zugangsdaten von bestehenden Passwort-Managern zu importieren, melde Dich bitte in die Web-App an. Die Import-Funktion ist derzeit nur auf der Web-Version verfügbar.",
|
||||
"exportSectionTitle": "Exportieren",
|
||||
"exportSectionDescription": "Exportiere Deinen Tresor in eine CSV-Datei. Diese Datei kann als Sicherung verwendet werden und kann auch in andere Passwort-Manager importiert werden.",
|
||||
"exportCsvButton": "Tresor als CSV-Datei exportieren",
|
||||
"exporting": "Wird exportiert...",
|
||||
"exportConfirmTitle": "Tresor exportieren",
|
||||
"exportWarning": "Warnung: Beim Exportieren Deines Tresors in eine unverschlüsselte Datei werden alle Deine Passwörter und sensiblen Daten im Klartext sichtbar.\nFühre diesen Vorgang nur auf vertrauenswürdigen Geräten aus und stelle sicher, dass Du:\n\n• die exportierte Datei an einem sicheren Ort speicherst.\n• die Datei löschst, sobald Du sie nicht mehr brauchst.\n• die Datei niemals mit anderen teilst.\n\nBist Du sicher, dass Du mit dem Export fortfahren möchtest?",
|
||||
"security": "Sicherheit",
|
||||
"appVersion": "App-Version {{version}} ({{url}})",
|
||||
"autoLockOptions": {
|
||||
@@ -275,6 +285,10 @@
|
||||
"genderUpdateFailed": "Die Geschlechtseinstellung konnte nicht aktualisiert werden."
|
||||
}
|
||||
},
|
||||
"passwordGeneratorSettings": {
|
||||
"description": "Konfiguriere die Standardeinstellungen für die Generierung neuer Passwörter. Diese Einstellungen werden für alle neuen Passwörter verwendet, sofern sie nicht für einzelne Einträge überschrieben werden.",
|
||||
"preview": "Vorschau"
|
||||
},
|
||||
"securitySettings": {
|
||||
"title": "Sicherheit",
|
||||
"description": "Verwalte Deine Sicherheitseinstellungen für Dein Konto und den Tresor.",
|
||||
@@ -424,12 +438,16 @@
|
||||
"openingVaultReadOnly": "Tresor wird im Lese-Modus geöffnet",
|
||||
"retryingConnection": "Verbindungsversuch wird wiederholt..."
|
||||
},
|
||||
"offline": {
|
||||
"banner": "Offline-Modus (nur lesender Zugriff)",
|
||||
"backOnline": "Wieder online",
|
||||
"stillOffline": "Immer noch offline"
|
||||
},
|
||||
"alerts": {
|
||||
"syncIssue": "Synchronisierungsproblem",
|
||||
"syncIssueMessage": "Der AliasVault-Server konnte nicht erreicht werden und Dein Tresor konnte nicht synchronisiert werden. Möchtest Du Deinen lokalen Tresor im Lese-Modus öffnen oder die Verbindung erneut versuchen",
|
||||
"openLocalVault": "Lokalen Tresor öffnen",
|
||||
"retrySync": "Synchronisierung erneut versuchen",
|
||||
"error": "Fehler"
|
||||
"retrySync": "Synchronisierung erneut versuchen"
|
||||
},
|
||||
"navigation": {
|
||||
"login": "Anmelden",
|
||||
@@ -467,7 +485,6 @@
|
||||
"whatsNew": "Neu in dieser Version",
|
||||
"whatsNewDescription": "Eine Aktualisierung ist erforderlich, um die folgenden Änderungen zu unterstützen:",
|
||||
"noDescriptionAvailable": "Für diese Version ist keine Beschreibung vorhanden.",
|
||||
"okay": "OK",
|
||||
"status": {
|
||||
"preparingUpgrade": "Aktualisierung wird vorbereitet...",
|
||||
"vaultAlreadyUpToDate": "Tresor ist bereits aktualisiert",
|
||||
@@ -477,11 +494,9 @@
|
||||
"committingChanges": "Änderungen werden übernommen..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Fehler",
|
||||
"unableToGetVersionInfo": "Versionsinformationen konnten nicht abgerufen werden. Bitte versuche es erneut.",
|
||||
"selfHostedServer": "Selbstgehosteter Server",
|
||||
"selfHostedWarning": "Nutzt Du einen selbst gehosteten Server, musst Du Deine Instanz ebenfalls updaten. Andernfalls kannst Du Dich im Web-Client nicht mehr anmelden.",
|
||||
"cancel": "Abbrechen",
|
||||
"continueUpgrade": "Aktualisierung fortsetzen",
|
||||
"upgradeFailed": "Aktualisierung fehlgeschlagen",
|
||||
"failedToApplyMigration": "Migration fehlgeschlagen ({{current}} von {{total}})",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user