mirror of
https://github.com/bitwarden/mobile
synced 2025-12-10 13:23:39 +00:00
Compare commits
68 Commits
bug/ps-675
...
v2022.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee27900c3a | ||
|
|
0723227eca | ||
|
|
099d7e22e2 | ||
|
|
c86d4e7984 | ||
|
|
6164106c84 | ||
|
|
525288d804 | ||
|
|
e829279e29 | ||
|
|
3d9555d420 | ||
|
|
5f7a1e769a | ||
|
|
8b118408fa | ||
|
|
de41845e3e | ||
|
|
61597585b5 | ||
|
|
e04b250a73 | ||
|
|
4fbe1b40e3 | ||
|
|
3ef5b576ac | ||
|
|
570b56364a | ||
|
|
ae4e8e2d8e | ||
|
|
2c8406d0ad | ||
|
|
94bd5ceed3 | ||
|
|
aa6be3d691 | ||
|
|
97fe65647a | ||
|
|
ee8b8866e0 | ||
|
|
3128a4c5c8 | ||
|
|
8ec6545bbc | ||
|
|
90a6850d76 | ||
|
|
16f70dc0ce | ||
|
|
f0ebc5e644 | ||
|
|
03c5dd78c1 | ||
|
|
e2b6e99a0c | ||
|
|
263aeef030 | ||
|
|
f809170c51 | ||
|
|
c2fcc0ac52 | ||
|
|
5e61fb0a14 | ||
|
|
cf222bd0c3 | ||
|
|
cb0c52fb26 | ||
|
|
c07c305384 | ||
|
|
d2fbf5bdea | ||
|
|
2d2a883b96 | ||
|
|
1f2fb3f796 | ||
|
|
8f3a4b98a5 | ||
|
|
70cf7431f7 | ||
|
|
f2ba86a62b | ||
|
|
292908f53f | ||
|
|
d621a5d2f3 | ||
|
|
75e8276784 | ||
|
|
67f49a0591 | ||
|
|
cceded2a0f | ||
|
|
846d3a85a2 | ||
|
|
7802da2b9c | ||
|
|
cd56a124d5 | ||
|
|
58a3662d0f | ||
|
|
6c7413e38c | ||
|
|
547e61a66b | ||
|
|
d246d1dece | ||
|
|
e2502e2e0c | ||
|
|
448cce38e1 | ||
|
|
dbc1e5ea3e | ||
|
|
a6ddc2496f | ||
|
|
d9a818279f | ||
|
|
6e2e613fee | ||
|
|
109aeb49e4 | ||
|
|
c892e9fa57 | ||
|
|
b2500557e7 | ||
|
|
7c311fbb55 | ||
|
|
f24388c1b5 | ||
|
|
3aef86bd34 | ||
|
|
c53a85cd50 | ||
|
|
448758a697 |
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -57,12 +57,37 @@ jobs:
|
|||||||
|
|
||||||
android:
|
android:
|
||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
|
run: |
|
||||||
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
|
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||||
|
$componentsToAdd = @(
|
||||||
|
"Component.Xamarin"
|
||||||
|
)
|
||||||
|
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
||||||
|
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
||||||
|
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($process.ExitCode -eq 0)
|
||||||
|
{
|
||||||
|
Write-Host "components have been successfully added"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write-Host "components were not installed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -207,11 +232,36 @@ jobs:
|
|||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Build
|
name: F-Droid Build
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
|
run: |
|
||||||
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
|
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||||
|
$componentsToAdd = @(
|
||||||
|
"Component.Xamarin"
|
||||||
|
)
|
||||||
|
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
||||||
|
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
||||||
|
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($process.ExitCode -eq 0)
|
||||||
|
{
|
||||||
|
Write-Host "components have been successfully added"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write-Host "components were not installed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -368,6 +418,11 @@ jobs:
|
|||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
|
|||||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
|
|||||||
56
.github/workflows/release.yml
vendored
56
.github/workflows/release.yml
vendored
@@ -13,6 +13,11 @@ on:
|
|||||||
- Initial Release
|
- Initial Release
|
||||||
- Redeploy
|
- Redeploy
|
||||||
- Dry Run
|
- Dry Run
|
||||||
|
fdroid_publish:
|
||||||
|
description: 'Publish to f-droid store'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -48,18 +53,38 @@ jobs:
|
|||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
echo "::set-output name=branch-name::$BRANCH_NAME"
|
||||||
|
|
||||||
|
- name: Create GitHub deployment
|
||||||
|
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
initial-status: 'in_progress'
|
||||||
|
environment: 'production'
|
||||||
|
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
|
||||||
|
task: release
|
||||||
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ steps.branch.outputs.branch-name }}
|
branch: ${{ steps.branch.outputs.branch-name }}
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
|
||||||
- name: Prep Bitwarden iOS release asset
|
- name: Prep Bitwarden iOS release asset
|
||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: github.event.inputs.release_type != 'Dry Run'
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
||||||
with:
|
with:
|
||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
@@ -73,16 +98,34 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
|
|
||||||
|
- name: Update deployment status to Success
|
||||||
|
if: ${{ success() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
state: 'success'
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
|
- name: Update deployment status to Failure
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
state: 'failure'
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Release
|
name: F-Droid Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: release
|
needs: release
|
||||||
|
if: inputs.fdroid_publish
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
@@ -90,6 +133,15 @@ jobs:
|
|||||||
branch: ${{ needs.release.outputs.branch-name }}
|
branch: ${{ needs.release.outputs.branch-name }}
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
|
|
||||||
|
- name: Download F-Droid .apk artifact
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
||||||
with:
|
with:
|
||||||
@@ -155,5 +207,5 @@ jobs:
|
|||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
- name: Deploy to gh-pages
|
- name: Deploy to gh-pages
|
||||||
if: github.event.inputs.release_type != 'Dry Run'
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
run: npm run deploy
|
run: npm run deploy
|
||||||
|
|||||||
67
.github/workflows/version-auto-bump.yml
vendored
Normal file
67
.github/workflows/version-auto-bump.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: Version Auto Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
setup:
|
||||||
|
name: "Setup"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
outputs:
|
||||||
|
version_number: ${{ steps.version.outputs.new-version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
- name: Get version to bump
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.event.release.tag }}
|
||||||
|
run: |
|
||||||
|
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
|
||||||
|
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
|
||||||
|
echo $CURR_VER
|
||||||
|
|
||||||
|
((CURR_VER++))
|
||||||
|
NEW_VER=$CURR_MAJOR$CURR_VER
|
||||||
|
|
||||||
|
echo $NEW_VER
|
||||||
|
|
||||||
|
echo "::set-output name=new-version::$NEW_VER"
|
||||||
|
|
||||||
|
trigger_version_bump:
|
||||||
|
name: "Trigger version bump workflow"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
steps:
|
||||||
|
- name: Login to Azure
|
||||||
|
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
env:
|
||||||
|
KEYVAULT: bitwarden-prod-kv
|
||||||
|
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
run: |
|
||||||
|
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
|
||||||
|
echo "::add-mask::$VALUE"
|
||||||
|
echo "::set-output name=$SECRET::$VALUE"
|
||||||
|
|
||||||
|
- name: Call GitHub API to trigger workflow bump
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
VERSION: ${{ needs.setup.outputs.version_number}}
|
||||||
|
run: |
|
||||||
|
JSON_STRING=$(printf '{"ref":"master", "inputs": { "version_number":"%s"}}' "$VERSION")
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
-i -u bitwarden-devops-bot:$TOKEN \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
https://api.github.com/repos/bitwarden/mobile/actions/workflows/version-bump.yml/dispatches \
|
||||||
|
-d $JSON_STRING
|
||||||
24
.github/workflows/version-bump.yml
vendored
24
.github/workflows/version-bump.yml
vendored
@@ -19,12 +19,6 @@ jobs:
|
|||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
run: |
|
run: |
|
||||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
|
||||||
|
|
||||||
- name: Checkout Version Branch
|
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
|
||||||
with:
|
|
||||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
|
||||||
|
|
||||||
- name: Bump Version - Android XML
|
- name: Bump Version - Android XML
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||||
@@ -56,16 +50,32 @@ jobs:
|
|||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ github.event.inputs.version_number }}
|
||||||
file_path: "./src/iOS/Info.plist"
|
file_path: "./src/iOS/Info.plist"
|
||||||
|
|
||||||
- name: Commit files
|
- name: Setup git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git config --local user.name "github-actions[bot]"
|
git config --local user.name "github-actions[bot]"
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: version-changed
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "::set-output name=changes_to_commit::TRUE"
|
||||||
|
else
|
||||||
|
echo "::set-output name=changes_to_commit::FALSE"
|
||||||
|
echo "No changes to commit!";
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit files
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
run: |
|
||||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
- name: Create Version PR
|
- name: Create Version PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
|
|||||||
|
|
||||||
# Build/Run
|
# Build/Run
|
||||||
|
|
||||||
Please refer to the [Mobile section](https://contributing.bitwarden.com/clients/mobile) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
# We're Hiring!
|
# We're Hiring!
|
||||||
|
|
||||||
|
|||||||
22
renovate.json
Normal file
22
renovate.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base",
|
||||||
|
"schedule:monthly",
|
||||||
|
":maintainLockFilesMonthly",
|
||||||
|
":preserveSemverRanges",
|
||||||
|
":rebaseStalePrs",
|
||||||
|
":disableDependencyDashboard"
|
||||||
|
],
|
||||||
|
"enabledManagers": [
|
||||||
|
"nuget"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["nuget"],
|
||||||
|
"groupName": "Nuget updates",
|
||||||
|
"groupSlug": "nuget",
|
||||||
|
"separateMajorMinor": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.google.android.apps.chrome", "url_bar"),
|
new Browser("com.google.android.apps.chrome", "url_bar"),
|
||||||
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
||||||
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
|
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
|
||||||
|
new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"),
|
||||||
new Browser("com.jamal2367.styx", "search"),
|
new Browser("com.jamal2367.styx", "search"),
|
||||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||||
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
|
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
|
||||||
@@ -67,6 +68,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.naver.whale", "url_bar"),
|
new Browser("com.naver.whale", "url_bar"),
|
||||||
new Browser("com.opera.browser", "url_field"),
|
new Browser("com.opera.browser", "url_field"),
|
||||||
new Browser("com.opera.browser.beta", "url_field"),
|
new Browser("com.opera.browser.beta", "url_field"),
|
||||||
|
new Browser("com.opera.gx", "addressbarEdit"),
|
||||||
new Browser("com.opera.mini.native", "url_field"),
|
new Browser("com.opera.mini.native", "url_field"),
|
||||||
new Browser("com.opera.mini.native.beta", "url_field"),
|
new Browser("com.opera.mini.native.beta", "url_field"),
|
||||||
new Browser("com.opera.touch", "addressbarEdit"),
|
new Browser("com.opera.touch", "addressbarEdit"),
|
||||||
|
|||||||
@@ -151,6 +151,7 @@
|
|||||||
<Compile Include="Services\ClipboardService.cs" />
|
<Compile Include="Services\ClipboardService.cs" />
|
||||||
<Compile Include="Utilities\IntentExtensions.cs" />
|
<Compile Include="Utilities\IntentExtensions.cs" />
|
||||||
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
||||||
|
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.google.android.apps.chrome",
|
"com.google.android.apps.chrome",
|
||||||
"com.google.android.apps.chrome_dev",
|
"com.google.android.apps.chrome_dev",
|
||||||
"com.google.android.captiveportallogin",
|
"com.google.android.captiveportallogin",
|
||||||
|
"com.iode.firefox",
|
||||||
"com.jamal2367.styx",
|
"com.jamal2367.styx",
|
||||||
"com.kiwibrowser.browser",
|
"com.kiwibrowser.browser",
|
||||||
"com.kiwibrowser.browser.dev",
|
"com.kiwibrowser.browser.dev",
|
||||||
@@ -86,6 +87,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.naver.whale",
|
"com.naver.whale",
|
||||||
"com.opera.browser",
|
"com.opera.browser",
|
||||||
"com.opera.browser.beta",
|
"com.opera.browser.beta",
|
||||||
|
"com.opera.gx",
|
||||||
"com.opera.mini.native",
|
"com.opera.mini.native",
|
||||||
"com.opera.mini.native.beta",
|
"com.opera.mini.native.beta",
|
||||||
"com.opera.touch",
|
"com.opera.touch",
|
||||||
|
|||||||
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Android.Widget;
|
||||||
|
using Bit.Droid.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
|
[assembly: ExportEffect(typeof(NoEmojiKeyboardEffect), nameof(NoEmojiKeyboardEffect))]
|
||||||
|
namespace Bit.Droid.Effects
|
||||||
|
{
|
||||||
|
public class NoEmojiKeyboardEffect : PlatformEffect
|
||||||
|
{
|
||||||
|
protected override void OnAttached()
|
||||||
|
{
|
||||||
|
if (Control is EditText editText)
|
||||||
|
{
|
||||||
|
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetached()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -64,10 +64,11 @@ namespace Bit.Droid
|
|||||||
Intent?.Validate();
|
Intent?.Validate();
|
||||||
|
|
||||||
base.OnCreate(savedInstanceState);
|
base.OnCreate(savedInstanceState);
|
||||||
if (!CoreHelpers.InDebugMode())
|
|
||||||
|
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
|
||||||
{
|
{
|
||||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||||
}
|
});
|
||||||
|
|
||||||
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using System.Net;
|
|||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Pages;
|
using Bit.App.Pages;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
|
using Bit.App.Controls;
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using Android.Gms.Security;
|
using Android.Gms.Security;
|
||||||
#endif
|
#endif
|
||||||
@@ -69,7 +70,8 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
|
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
|
||||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||||
ServiceContainer.Resolve<IAuthService>("authService"));
|
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||||
|
ServiceContainer.Resolve<ILogger>("logger"));
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
@@ -99,12 +101,13 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
||||||
#if FDROID
|
#if FDROID
|
||||||
ServiceContainer.Register<ILogger>("logger", new StubLogger());
|
var logger = new StubLogger();
|
||||||
#elif DEBUG
|
#elif DEBUG
|
||||||
ServiceContainer.Register<ILogger>("logger", DebugLogger.Instance);
|
var logger = DebugLogger.Instance;
|
||||||
#else
|
#else
|
||||||
ServiceContainer.Register<ILogger>("logger", Logger.Instance);
|
var logger = Logger.Instance;
|
||||||
#endif
|
#endif
|
||||||
|
ServiceContainer.Register("logger", logger);
|
||||||
|
|
||||||
// Note: This might cause a race condition. Investigate more.
|
// Note: This might cause a race condition. Investigate more.
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
@@ -124,13 +127,13 @@ namespace Bit.Droid
|
|||||||
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
||||||
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
||||||
var localizeService = new LocalizeService();
|
var localizeService = new LocalizeService();
|
||||||
var broadcasterService = new BroadcasterService();
|
var broadcasterService = new BroadcasterService(logger);
|
||||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||||
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
||||||
var secureStorageService = new SecureStorageService();
|
var secureStorageService = new SecureStorageService();
|
||||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||||
var stateService = new StateService(mobileStorageService, secureStorageService);
|
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
|
||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
@@ -159,6 +162,7 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||||
|
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
#if FDROID
|
#if FDROID
|
||||||
|
|||||||
@@ -1,57 +1,49 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.05.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.9.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.NFC"/>
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
|
||||||
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
|
</provider>
|
||||||
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
|
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
|
||||||
</provider>
|
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
||||||
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true" />
|
||||||
<meta-data android:name="android.max_aspect" android:value="2.1"/>
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true" />
|
||||||
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions"/>
|
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
||||||
|
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
|
||||||
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
||||||
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true"/>
|
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
|
||||||
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true"/>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true"/>
|
</intent-filter>
|
||||||
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
<intent-filter>
|
||||||
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
|
<action android:name="android.intent.action.SEND" />
|
||||||
<intent-filter>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<data android:mimeType="application/*" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
<data android:mimeType="video/*" />
|
||||||
<intent-filter>
|
<data android:mimeType="text/*" />
|
||||||
<action android:name="android.intent.action.SEND"/>
|
</intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
</activity>
|
||||||
<data android:mimeType="application/*"/>
|
</application>
|
||||||
<data android:mimeType="image/*"/>
|
<!-- Package visibility (for Android 11+) -->
|
||||||
<data android:mimeType="video/*"/>
|
<queries>
|
||||||
<data android:mimeType="text/*"/>
|
<intent>
|
||||||
</intent-filter>
|
<action android:name="*" />
|
||||||
</activity>
|
</intent>
|
||||||
</application>
|
</queries>
|
||||||
|
</manifest>
|
||||||
<!-- Package visibility (for Android 11+) -->
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="*"/>
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -77,6 +77,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.google.android.captiveportallogin"
|
android:name="com.google.android.captiveportallogin"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.iode.firefox"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.jamal2367.styx"
|
android:name="com.jamal2367.styx"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -116,6 +119,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.opera.browser.beta"
|
android:name="com.opera.browser.beta"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.opera.gx"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.opera.mini.native"
|
android:name="com.opera.mini.native"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
|||||||
@@ -28,17 +28,25 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||||
{
|
{
|
||||||
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
|
try
|
||||||
if ((int)Build.VERSION.SdkInt < 33)
|
|
||||||
{
|
{
|
||||||
await Clipboard.SetTextAsync(text);
|
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
|
||||||
}
|
if ((int)Build.VERSION.SdkInt < 33)
|
||||||
else
|
{
|
||||||
{
|
await Clipboard.SetTextAsync(text);
|
||||||
CopyToClipboard(text, isSensitive);
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
CopyToClipboard(text, isSensitive);
|
||||||
|
}
|
||||||
|
|
||||||
await ClearClipboardAlarmAsync(expiresInMs);
|
await ClearClipboardAlarmAsync(expiresInMs);
|
||||||
|
}
|
||||||
|
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
|
||||||
|
{
|
||||||
|
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
|
||||||
|
// that the OS catches and just throws this exception.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCopyNotificationHandledByPlatform()
|
public bool IsCopyNotificationHandledByPlatform()
|
||||||
|
|||||||
@@ -948,5 +948,21 @@ namespace Bit.Droid.Services
|
|||||||
{
|
{
|
||||||
// for any Android-specific cleanup required after switching accounts
|
// for any Android-specific cleanup required after switching accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetScreenCaptureAllowedAsync()
|
||||||
|
{
|
||||||
|
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity = CrossCurrentActivity.Current?.Activity;
|
||||||
|
if (await _stateService.GetScreenCaptureAllowedAsync())
|
||||||
|
{
|
||||||
|
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ namespace Bit.Droid.Utilities
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
||||||
{
|
{
|
||||||
theme = "dark";
|
theme = ThemeManager.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme == "dark" || theme == "black" || theme == "nord")
|
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
|
||||||
{
|
{
|
||||||
LightTheme = false;
|
LightTheme = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ namespace Bit.App.Abstractions
|
|||||||
{
|
{
|
||||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||||
|
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,6 @@ namespace Bit.App.Abstractions
|
|||||||
bool SupportsFido2();
|
bool SupportsFido2();
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
|
Task SetScreenCaptureAllowedAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,11 @@
|
|||||||
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
||||||
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
|
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
|
||||||
<DependentUpon>AddEditPage.xaml</DependentUpon>
|
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
|
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
|
||||||
<DependentUpon>ViewPage.xaml</DependentUpon>
|
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
||||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||||
@@ -129,12 +129,10 @@
|
|||||||
<Folder Include="Behaviors\" />
|
<Folder Include="Behaviors\" />
|
||||||
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
||||||
<Folder Include="Utilities\AccountManagement\" />
|
<Folder Include="Utilities\AccountManagement\" />
|
||||||
|
<Folder Include="Controls\DateTime\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
|
|
||||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
|
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -162,12 +160,6 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Styles\Base.xaml">
|
|
||||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Resources\AppResources.cs.Designer.cs">
|
<Compile Update="Resources\AppResources.cs.Designer.cs">
|
||||||
<DependentUpon>AppResources.cs.resx</DependentUpon>
|
<DependentUpon>AppResources.cs.resx</DependentUpon>
|
||||||
@@ -422,5 +414,6 @@
|
|||||||
<None Remove="Xamarin.CommunityToolkit" />
|
<None Remove="Xamarin.CommunityToolkit" />
|
||||||
<None Remove="Controls\AccountSwitchingOverlay\" />
|
<None Remove="Controls\AccountSwitchingOverlay\" />
|
||||||
<None Remove="Utilities\AccountManagement\" />
|
<None Remove="Utilities\AccountManagement\" />
|
||||||
|
<None Remove="Controls\DateTime\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Bit.App.Utilities.AccountManagement;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms.Xaml;
|
using Xamarin.Forms.Xaml;
|
||||||
@@ -56,86 +57,93 @@ namespace Bit.App
|
|||||||
Bootstrap();
|
Bootstrap();
|
||||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "showDialog")
|
try
|
||||||
{
|
{
|
||||||
var details = message.Data as DialogDetails;
|
if (message.Command == "showDialog")
|
||||||
var confirmed = true;
|
|
||||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
|
||||||
AppResources.Ok : details.ConfirmText;
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
var details = message.Data as DialogDetails;
|
||||||
|
var confirmed = true;
|
||||||
|
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||||
|
AppResources.Ok : details.ConfirmText;
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
{
|
{
|
||||||
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||||
details.CancelText);
|
{
|
||||||
}
|
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||||
else
|
details.CancelText);
|
||||||
{
|
}
|
||||||
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
else
|
||||||
}
|
{
|
||||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||||
});
|
}
|
||||||
}
|
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||||
else if (message.Command == "resumed")
|
});
|
||||||
{
|
}
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
else if (message.Command == "resumed")
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ResumedAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == "slept")
|
||||||
|
{
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
await SleptAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == "migrated")
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
||||||
|
message.Command == "popAllAndGoToTabMyVault" ||
|
||||||
|
message.Command == "popAllAndGoToTabSend" ||
|
||||||
|
message.Command == "popAllAndGoToAutofillCiphers")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
if (Current.MainPage is TabsPage tabsPage)
|
||||||
|
{
|
||||||
|
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||||
|
{
|
||||||
|
await tabsPage.Navigation.PopModalAsync(false);
|
||||||
|
}
|
||||||
|
if (message.Command == "popAllAndGoToAutofillCiphers")
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabMyVault")
|
||||||
|
{
|
||||||
|
Options.MyVaultTile = false;
|
||||||
|
tabsPage.ResetToVaultPage();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabGenerator")
|
||||||
|
{
|
||||||
|
Options.GeneratorTile = false;
|
||||||
|
tabsPage.ResetToGeneratorPage();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabSend")
|
||||||
|
{
|
||||||
|
tabsPage.ResetToSendPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (message.Command == "convertAccountToKeyConnector")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
await Application.Current.MainPage.Navigation.PushModalAsync(
|
||||||
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "slept")
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
{
|
|
||||||
await SleptAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.Command == "migrated")
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
|
||||||
message.Command == "popAllAndGoToTabMyVault" ||
|
|
||||||
message.Command == "popAllAndGoToTabSend" ||
|
|
||||||
message.Command == "popAllAndGoToAutofillCiphers")
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
if (Current.MainPage is TabsPage tabsPage)
|
|
||||||
{
|
|
||||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
|
||||||
{
|
|
||||||
await tabsPage.Navigation.PopModalAsync(false);
|
|
||||||
}
|
|
||||||
if (message.Command == "popAllAndGoToAutofillCiphers")
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabMyVault")
|
|
||||||
{
|
|
||||||
Options.MyVaultTile = false;
|
|
||||||
tabsPage.ResetToVaultPage();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
|
||||||
{
|
|
||||||
Options.GeneratorTile = false;
|
|
||||||
tabsPage.ResetToGeneratorPage();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabSend")
|
|
||||||
{
|
|
||||||
tabsPage.ResetToSendPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (message.Command == "convertAccountToKeyConnector")
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
await Application.Current.MainPage.Navigation.PushModalAsync(
|
|
||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -202,6 +210,7 @@ namespace Bit.App
|
|||||||
|
|
||||||
private async Task ResumedAsync()
|
private async Task ResumedAsync()
|
||||||
{
|
{
|
||||||
|
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
_messagingService.Send("startEventTimer");
|
_messagingService.Send("startEventTimer");
|
||||||
await UpdateThemeAsync();
|
await UpdateThemeAsync();
|
||||||
@@ -292,7 +301,7 @@ namespace Bit.App
|
|||||||
UpdateThemeAsync();
|
UpdateThemeAsync();
|
||||||
};
|
};
|
||||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||||
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync();
|
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,20 +330,20 @@ namespace Bit.App
|
|||||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||||
if (topPage is NavigationPage navPage)
|
if (topPage is NavigationPage navPage)
|
||||||
{
|
{
|
||||||
if (navPage.CurrentPage is ViewPage viewPage)
|
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "view",
|
Page = "view",
|
||||||
CipherId = viewPage.ViewModel.CipherId
|
CipherId = cipherDetailsPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "edit",
|
Page = "edit",
|
||||||
CipherId = addEditPage.ViewModel.CipherId
|
CipherId = cipherAddEditPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +378,7 @@ namespace Bit.App
|
|||||||
Current.MainPage = new TabsPage(Options);
|
Current.MainPage = new TabsPage(Options);
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AddEditCipher:
|
case NavigationTarget.AddEditCipher:
|
||||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AutofillCiphers:
|
case NavigationTarget.AutofillCiphers:
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
public bool LongPressAccountEnabled { get; set; } = true;
|
public bool LongPressAccountEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
public Action AfterHide { get; set; }
|
||||||
|
|
||||||
public async Task ToggleVisibilityAsync()
|
public async Task ToggleVisibilityAsync()
|
||||||
{
|
{
|
||||||
if (IsVisible)
|
if (IsVisible)
|
||||||
@@ -137,6 +139,8 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
// remove overlay
|
// remove overlay
|
||||||
IsVisible = false;
|
IsVisible = false;
|
||||||
|
|
||||||
|
AfterHide?.Invoke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,23 +45,28 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
public ICommand LongPressAccountCommand { get; }
|
public ICommand LongPressAccountCommand { get; }
|
||||||
|
|
||||||
|
public bool FromIOSExtension { get; set; }
|
||||||
|
|
||||||
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
||||||
{
|
{
|
||||||
if (item.AccountView.IsAccount)
|
if (!item.AccountView.IsAccount)
|
||||||
{
|
{
|
||||||
if (!item.AccountView.IsActive)
|
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.AccountView.IsActive)
|
||||||
|
{
|
||||||
|
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
||||||
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
|
if (FromIOSExtension)
|
||||||
{
|
{
|
||||||
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
|
||||||
_messagingService.Send("switchedAccount");
|
|
||||||
}
|
|
||||||
else if (AllowActiveAccountSelection)
|
|
||||||
{
|
|
||||||
_messagingService.Send("switchedAccount");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (AllowActiveAccountSelection)
|
||||||
{
|
{
|
||||||
_messagingService.Send("addAccount");
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace Bit.App.Controls
|
|||||||
public AccountViewCellViewModel(AccountView accountView)
|
public AccountViewCellViewModel(AccountView accountView)
|
||||||
{
|
{
|
||||||
AccountView = accountView;
|
AccountView = accountView;
|
||||||
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
|
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||||
|
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountView AccountView
|
public AccountView AccountView
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Controls.AuthenticatorViewCell"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
StyleClass="list-row, list-row-platform"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
x:DataType="pages:GroupingsPageTOTPListItem"
|
||||||
|
ColumnDefinitions="40,*,40,Auto,40"
|
||||||
|
RowSpacing="0"
|
||||||
|
Padding="0,10,0,0"
|
||||||
|
RowDefinitions="*,*">
|
||||||
|
|
||||||
|
<Grid.Resources>
|
||||||
|
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
</Grid.Resources>
|
||||||
|
|
||||||
|
<controls:IconLabel
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
StyleClass="list-icon, list-icon-platform"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||||
|
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="False" />
|
||||||
|
|
||||||
|
<ff:CachedImage
|
||||||
|
Grid.Column="0"
|
||||||
|
BitmapOptimizations="True"
|
||||||
|
ErrorPlaceholder="login.png"
|
||||||
|
LoadingPlaceholder="login.png"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
WidthRequest="22"
|
||||||
|
HeightRequest="22"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
IsVisible="{Binding ShowIconImage}"
|
||||||
|
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="False" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill"
|
||||||
|
StyleClass="list-title, list-title-platform"
|
||||||
|
Text="{Binding Cipher.Name}" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill"
|
||||||
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
|
Text="{Binding Cipher.SubTitle}" />
|
||||||
|
|
||||||
|
<controls:CircularProgressbarView
|
||||||
|
Progress="{Binding Progress}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
HorizontalOptions="Fill"
|
||||||
|
VerticalOptions="CenterAndExpand" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
Text="{Binding TotpSec, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Fill"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill" />
|
||||||
|
|
||||||
|
<StackLayout
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="3,0,2,0"
|
||||||
|
Spacing="5"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalOptions="Fill"
|
||||||
|
VerticalOptions="Fill">
|
||||||
|
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
StyleClass="text-lg"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
StyleClass="text-lg"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
|
Command="{Binding CopyCommand}"
|
||||||
|
CommandParameter="LoginTotp"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="4"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Padding="0,0,1,0"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||||
|
</controls:ExtendedGrid>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.App.Pages;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class AuthenticatorViewCell : ExtendedGrid
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
|
||||||
|
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
|
||||||
|
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
|
||||||
|
|
||||||
|
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
|
||||||
|
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
|
||||||
|
|
||||||
|
public AuthenticatorViewCell()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command CopyCommand { get; set; }
|
||||||
|
|
||||||
|
public CipherView Cipher
|
||||||
|
{
|
||||||
|
get => GetValue(CipherProperty) as CipherView;
|
||||||
|
set => SetValue(CipherProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool? WebsiteIconsEnabled
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(WebsiteIconsEnabledProperty);
|
||||||
|
set => SetValue(WebsiteIconsEnabledProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotpSec
|
||||||
|
{
|
||||||
|
get => (long)GetValue(TotpSecProperty);
|
||||||
|
set => SetValue(TotpSecProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowIconImage
|
||||||
|
{
|
||||||
|
get => WebsiteIconsEnabled ?? false
|
||||||
|
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||||
|
&& IconImageSource != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _iconImageSource = string.Empty;
|
||||||
|
public string IconImageSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||||
|
{
|
||||||
|
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||||
|
}
|
||||||
|
return _iconImageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@@ -50,7 +51,7 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
private Stream Draw()
|
private Stream Draw()
|
||||||
{
|
{
|
||||||
string chars = null;
|
string chars;
|
||||||
string upperData = null;
|
string upperData = null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_data))
|
if (string.IsNullOrEmpty(_data))
|
||||||
@@ -71,62 +72,83 @@ namespace Bit.App.Controls
|
|||||||
var textColor = Color.White;
|
var textColor = Color.White;
|
||||||
var size = 50;
|
var size = 50;
|
||||||
|
|
||||||
var bitmap = new SKBitmap(
|
using (var bitmap = new SKBitmap(size * 2,
|
||||||
size * 2,
|
|
||||||
size * 2,
|
size * 2,
|
||||||
SKImageInfo.PlatformColorType,
|
SKImageInfo.PlatformColorType,
|
||||||
SKAlphaType.Premul);
|
SKAlphaType.Premul))
|
||||||
var canvas = new SKCanvas(bitmap);
|
|
||||||
canvas.Clear(SKColors.Transparent);
|
|
||||||
|
|
||||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
|
||||||
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
|
||||||
var radius = midX - midX / 5;
|
|
||||||
|
|
||||||
var circlePaint = new SKPaint
|
|
||||||
{
|
{
|
||||||
IsAntialias = true,
|
using (var canvas = new SKCanvas(bitmap))
|
||||||
Style = SKPaintStyle.Fill,
|
{
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
canvas.Clear(SKColors.Transparent);
|
||||||
Color = SKColor.Parse(bgColor.ToHex())
|
using (var paint = new SKPaint
|
||||||
};
|
{
|
||||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
IsAntialias = true,
|
||||||
|
Style = SKPaintStyle.Fill,
|
||||||
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
|
Color = SKColor.Parse(bgColor.ToHex())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||||
|
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
||||||
|
var radius = midX - midX / 5;
|
||||||
|
|
||||||
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
using (var circlePaint = new SKPaint
|
||||||
var textSize = midX / 1.3f;
|
{
|
||||||
var textPaint = new SKPaint
|
IsAntialias = true,
|
||||||
{
|
Style = SKPaintStyle.Fill,
|
||||||
IsAntialias = true,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Style = SKPaintStyle.Fill,
|
Color = SKColor.Parse(bgColor.ToHex())
|
||||||
Color = SKColor.Parse(textColor.ToHex()),
|
})
|
||||||
TextSize = textSize,
|
{
|
||||||
TextAlign = SKTextAlign.Center,
|
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||||
Typeface = typeface
|
|
||||||
};
|
|
||||||
var rect = new SKRect();
|
|
||||||
textPaint.MeasureText(chars, ref rect);
|
|
||||||
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
|
||||||
|
|
||||||
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
|
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
||||||
|
var textSize = midX / 1.3f;
|
||||||
|
using (var textPaint = new SKPaint
|
||||||
|
{
|
||||||
|
IsAntialias = true,
|
||||||
|
Style = SKPaintStyle.Fill,
|
||||||
|
Color = SKColor.Parse(textColor.ToHex()),
|
||||||
|
TextSize = textSize,
|
||||||
|
TextAlign = SKTextAlign.Center,
|
||||||
|
Typeface = typeface
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var rect = new SKRect();
|
||||||
|
textPaint.MeasureText(chars, ref rect);
|
||||||
|
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
||||||
|
|
||||||
|
using (var img = SKImage.FromBitmap(bitmap))
|
||||||
|
{
|
||||||
|
var data = img.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
|
return data?.AsStream(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFirstLetters(string data, int charCount)
|
private string GetFirstLetters(string data, int charCount)
|
||||||
{
|
{
|
||||||
var parts = data.Split();
|
var sanitizedData = data.Trim();
|
||||||
|
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
if (parts.Length > 1 && charCount <= 2)
|
if (parts.Length > 1 && charCount <= 2)
|
||||||
{
|
{
|
||||||
var text = "";
|
var text = string.Empty;
|
||||||
for (int i = 0; i < charCount; i++)
|
for (var i = 0; i < charCount; i++)
|
||||||
{
|
{
|
||||||
text += parts[i].Substring(0, 1);
|
text += parts[i][0];
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
if (data.Length > 2)
|
if (sanitizedData.Length > 2)
|
||||||
{
|
{
|
||||||
return data.Substring(0, 2);
|
return sanitizedData.Substring(0, 2);
|
||||||
}
|
}
|
||||||
return data;
|
return sanitizedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color StringToColor(string str)
|
private Color StringToColor(string str)
|
||||||
|
|||||||
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public interface IAvatarImageSourcePool
|
||||||
|
{
|
||||||
|
AvatarImageSource GetOrCreateAvatar(string name, string email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
||||||
|
|
||||||
|
public AvatarImageSource GetOrCreateAvatar(string name, string email)
|
||||||
|
{
|
||||||
|
var key = $"{name}{email}";
|
||||||
|
if (!_cache.TryGetValue(key, out var avatar))
|
||||||
|
{
|
||||||
|
avatar = new AvatarImageSource(name, email);
|
||||||
|
if (!_cache.TryAdd(key, avatar)
|
||||||
|
&&
|
||||||
|
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
||||||
|
{
|
||||||
|
// if add and get after fails, then something wrong is going on with this method.
|
||||||
|
throw new InvalidOperationException("Something is wrong creating the avatar image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
139
src/App/Controls/CircularProgressbarView.cs
Normal file
139
src/App/Controls/CircularProgressbarView.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.Views.Forms;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class CircularProgressbarView : SKCanvasView
|
||||||
|
{
|
||||||
|
private Circle _circle;
|
||||||
|
|
||||||
|
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
|
||||||
|
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
|
||||||
|
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
|
||||||
|
|
||||||
|
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
|
||||||
|
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public double Progress
|
||||||
|
{
|
||||||
|
get { return (double)GetValue(ProgressProperty); }
|
||||||
|
set { SetValue(ProgressProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Radius
|
||||||
|
{
|
||||||
|
get => (float)GetValue(RadiusProperty);
|
||||||
|
set => SetValue(RadiusProperty, value);
|
||||||
|
}
|
||||||
|
public float StrokeWidth
|
||||||
|
{
|
||||||
|
get => (float)GetValue(StrokeWidthProperty);
|
||||||
|
set => SetValue(StrokeWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color ProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(ProgressColorProperty);
|
||||||
|
set => SetValue(ProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color EndingProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(EndingProgressColorProperty);
|
||||||
|
set => SetValue(EndingProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color BackgroundProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(BackgroundProgressColorProperty);
|
||||||
|
set => SetValue(BackgroundProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
|
||||||
|
{
|
||||||
|
var context = bindable as CircularProgressbarView;
|
||||||
|
context.InvalidateSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
if (propertyName == nameof(Progress))
|
||||||
|
{
|
||||||
|
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaintSurface(e);
|
||||||
|
if (_circle != null)
|
||||||
|
{
|
||||||
|
_circle.CalculateCenter(e.Info);
|
||||||
|
e.Surface.Canvas.Clear();
|
||||||
|
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
|
||||||
|
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
|
||||||
|
{
|
||||||
|
canvas.DrawCircle(circle.Center, circle.Redius,
|
||||||
|
new SKPaint()
|
||||||
|
{
|
||||||
|
StrokeWidth = strokewidth,
|
||||||
|
Color = color,
|
||||||
|
IsStroke = true,
|
||||||
|
IsAntialias = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
|
||||||
|
{
|
||||||
|
var progressValue = progress();
|
||||||
|
var angle = progressValue * 3.6f;
|
||||||
|
canvas.DrawArc(circle.Rect, 270, angle, false,
|
||||||
|
new SKPaint()
|
||||||
|
{
|
||||||
|
StrokeWidth = strokewidth,
|
||||||
|
Color = progressValue < 20f ? progressEndColor : color,
|
||||||
|
IsStroke = true,
|
||||||
|
IsAntialias = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Circle
|
||||||
|
{
|
||||||
|
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
|
||||||
|
|
||||||
|
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
|
||||||
|
{
|
||||||
|
_centerFunc = centerFunc;
|
||||||
|
Redius = redius;
|
||||||
|
}
|
||||||
|
public SKPoint Center { get; set; }
|
||||||
|
public float Redius { get; set; }
|
||||||
|
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
|
||||||
|
|
||||||
|
public void CalculateCenter(SKImageInfo argsInfo)
|
||||||
|
{
|
||||||
|
Center = _centerFunc(argsInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Grid
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Class="Bit.App.Controls.DateTimePicker"
|
||||||
|
ColumnDefinitions="*,*">
|
||||||
|
<controls:ExtendedDatePicker
|
||||||
|
x:Name="_datePicker"
|
||||||
|
Grid.Column="0"
|
||||||
|
NullableDate="{Binding Date, Mode=TwoWay}"
|
||||||
|
Format="d"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True" />
|
||||||
|
<controls:ExtendedTimePicker
|
||||||
|
x:Name="_timePicker"
|
||||||
|
Grid.Column="1"
|
||||||
|
NullableTime="{Binding Time, Mode=TwoWay}"
|
||||||
|
Format="t"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True" />
|
||||||
|
</Grid>
|
||||||
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Xamarin.CommunityToolkit.UI.Views;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class DateTimePicker : Grid
|
||||||
|
{
|
||||||
|
public DateTimePicker()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
if (propertyName == nameof(BindingContext)
|
||||||
|
&&
|
||||||
|
BindingContext is DateTimeViewModel dateTimeViewModel)
|
||||||
|
{
|
||||||
|
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
|
||||||
|
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
|
||||||
|
|
||||||
|
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
|
||||||
|
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LazyDateTimePicker : LazyView<DateTimePicker>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class DateTimeViewModel : ExtendedViewModel
|
||||||
|
{
|
||||||
|
DateTime? _date;
|
||||||
|
TimeSpan? _time;
|
||||||
|
|
||||||
|
public DateTimeViewModel(string dateName, string timeName)
|
||||||
|
{
|
||||||
|
DateName = dateName;
|
||||||
|
TimeName = timeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<DateTime?> OnDateChanged { get; set; }
|
||||||
|
public Action<TimeSpan?> OnTimeChanged { get; set; }
|
||||||
|
|
||||||
|
public DateTime? Date
|
||||||
|
{
|
||||||
|
get => _date;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _date, value))
|
||||||
|
{
|
||||||
|
OnDateChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public TimeSpan? Time
|
||||||
|
{
|
||||||
|
get => _time;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _time, value))
|
||||||
|
{
|
||||||
|
OnTimeChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DateName { get; }
|
||||||
|
public string TimeName { get; }
|
||||||
|
|
||||||
|
public string DatePlaceholder { get; set; }
|
||||||
|
public string TimePlaceholder { get; set; }
|
||||||
|
|
||||||
|
public DateTime? DateTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Date.HasValue)
|
||||||
|
{
|
||||||
|
if (Time.HasValue)
|
||||||
|
{
|
||||||
|
return Date.Value.Add(Time.Value);
|
||||||
|
}
|
||||||
|
return Date;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Date = value?.Date;
|
||||||
|
Time = value?.Date.TimeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Effects
|
||||||
|
{
|
||||||
|
public class NoEmojiKeyboardEffect : RoutingEffect
|
||||||
|
{
|
||||||
|
public NoEmojiKeyboardEffect()
|
||||||
|
: base("Bitwarden.NoEmojiKeyboardEffect")
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
|
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ namespace Bit.App.Pages
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Submit_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
await _vm.SubmitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SubmitSuccessAsync()
|
private async Task SubmitSuccessAsync()
|
||||||
{
|
{
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class EnvironmentPageViewModel : BaseViewModel
|
public class EnvironmentPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IEnvironmentService _environmentService;
|
private readonly IEnvironmentService _environmentService;
|
||||||
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
public EnvironmentPageViewModel()
|
public EnvironmentPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -22,10 +24,10 @@ namespace Bit.App.Pages
|
|||||||
IdentityUrl = _environmentService.IdentityUrl;
|
IdentityUrl = _environmentService.IdentityUrl;
|
||||||
IconsUrl = _environmentService.IconsUrl;
|
IconsUrl = _environmentService.IconsUrl;
|
||||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; }
|
||||||
public string ApiUrl { get; set; }
|
public string ApiUrl { get; set; }
|
||||||
public string IdentityUrl { get; set; }
|
public string IdentityUrl { get; set; }
|
||||||
@@ -37,6 +39,12 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
|
if (!ValidateUrls())
|
||||||
|
{
|
||||||
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||||
{
|
{
|
||||||
Base = BaseUrl,
|
Base = BaseUrl,
|
||||||
@@ -57,5 +65,25 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
SubmitSuccessAction?.Invoke();
|
SubmitSuccessAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ValidateUrls()
|
||||||
|
{
|
||||||
|
bool IsUrlValid(string url)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsUrlValid(BaseUrl)
|
||||||
|
&& IsUrlValid(ApiUrl)
|
||||||
|
&& IsUrlValid(IdentityUrl)
|
||||||
|
&& IsUrlValid(WebVaultUrl)
|
||||||
|
&& IsUrlValid(IconsUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSubmitException(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -24,14 +23,6 @@ namespace Bit.App.Pages
|
|||||||
RequestFocus(_email);
|
RequestFocus(_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Submit_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
await _vm.SubmitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -13,18 +14,26 @@ namespace Bit.App.Pages
|
|||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public HintPageViewModel()
|
public HintPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
|
||||||
PageTitle = AppResources.PasswordHint;
|
PageTitle = AppResources.PasswordHint;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||||
|
onException: ex =>
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||||
|
},
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
@@ -37,14 +46,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Email.Contains("@"))
|
if (!Email.Contains("@"))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +63,7 @@ namespace Bit.App.Pages
|
|||||||
await _apiService.PostPasswordHintAsync(
|
await _apiService.PostPasswordHintAsync(
|
||||||
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||||
await Page.Navigation.PopModalAsync();
|
await Page.Navigation.PopModalAsync();
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
}
|
}
|
||||||
_broadcasterService.Subscribe(nameof(HomePage), async (message) =>
|
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "updatedTheme")
|
if (message.Command == "updatedTheme")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ namespace Bit.App.Pages
|
|||||||
_vm = BindingContext as LockPageViewModel;
|
_vm = BindingContext as LockPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||||
MasterPasswordEntry = _masterPassword;
|
|
||||||
PinEntry = _pin;
|
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
@@ -38,8 +36,17 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry MasterPasswordEntry { get; set; }
|
public Entry SecretEntry
|
||||||
public Entry PinEntry { get; set; }
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_vm?.PinLock ?? false)
|
||||||
|
{
|
||||||
|
return _pin;
|
||||||
|
}
|
||||||
|
return _masterPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAfterResumeAsync()
|
public async Task PromptBiometricAfterResumeAsync()
|
||||||
{
|
{
|
||||||
@@ -70,16 +77,12 @@ namespace Bit.App.Pages
|
|||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
|
|
||||||
await _vm.InitAsync();
|
await _vm.InitAsync();
|
||||||
|
|
||||||
|
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||||
|
|
||||||
if (!_vm.BiometricLock)
|
if (!_vm.BiometricLock)
|
||||||
{
|
{
|
||||||
if (_vm.PinLock)
|
RequestFocus(SecretEntry);
|
||||||
{
|
|
||||||
RequestFocus(PinEntry);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RequestFocus(MasterPasswordEntry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -99,6 +102,18 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PerformFocusSecretEntry(int? cursorPosition)
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
SecretEntry.Focus();
|
||||||
|
if (cursorPosition.HasValue)
|
||||||
|
{
|
||||||
|
SecretEntry.CursorPosition = cursorPosition.Value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
{
|
{
|
||||||
if (_accountListOverlay.IsVisible)
|
if (_accountListOverlay.IsVisible)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.Helpers;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -27,6 +28,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IBiometricService _biometricService;
|
private readonly IBiometricService _biometricService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||||
|
|
||||||
private string _email;
|
private string _email;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
@@ -133,6 +135,11 @@ namespace Bit.App.Pages
|
|||||||
public string MasterPassword { get; set; }
|
public string MasterPassword { get; set; }
|
||||||
public string Pin { get; set; }
|
public string Pin { get; set; }
|
||||||
public Action UnlockedAction { get; set; }
|
public Action UnlockedAction { get; set; }
|
||||||
|
public event Action<int?> FocusSecretEntry
|
||||||
|
{
|
||||||
|
add => _secretEntryFocusWeakEventManager.AddEventHandler(value);
|
||||||
|
remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
@@ -346,11 +353,8 @@ namespace Bit.App.Pages
|
|||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
var page = (Page as LockPage);
|
var secret = PinLock ? Pin : MasterPassword;
|
||||||
var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry;
|
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||||
var str = PinLock ? Pin : MasterPassword;
|
|
||||||
entry.Focus();
|
|
||||||
entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAsync()
|
public async Task PromptBiometricAsync()
|
||||||
@@ -361,18 +365,8 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
{
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||||
var page = Page as LockPage;
|
|
||||||
if (PinLock)
|
|
||||||
{
|
|
||||||
page.PinEntry.Focus();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
page.MasterPasswordEntry.Focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<Entry
|
<Entry
|
||||||
x:Name="_email"
|
x:Name="_email"
|
||||||
Text="{Binding Email}"
|
Text="{Binding Email}"
|
||||||
|
IsEnabled="{Binding IsEmailEnabled}"
|
||||||
Keyboard="Email"
|
Keyboard="Email"
|
||||||
StyleClass="box-value">
|
StyleClass="box-value">
|
||||||
<VisualStateManager.VisualStateGroups>
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private bool _inputFocused;
|
private bool _inputFocused;
|
||||||
|
|
||||||
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
public LoginPage(string email = null, AppOptions appOptions = null)
|
public LoginPage(string email = null, AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
@@ -30,11 +32,10 @@ namespace Bit.App.Pages
|
|||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
};
|
};
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
|
||||||
{
|
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
|
||||||
_email.IsEnabled = false;
|
|
||||||
}
|
if (_vm.IsEmailEnabled)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_vm.ShowCancelButton = true;
|
_vm.ShowCancelButton = true;
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_getPasswordHint);
|
ToolbarItems.Add(_getPasswordHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
|
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_removeAccount);
|
ToolbarItems.Add(_removeAccount);
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
await _vm.LogInAsync(true, _email.IsEnabled);
|
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,26 +140,16 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void More_Clicked(object sender, System.EventArgs e)
|
private async void More_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await _accountListOverlay.HideAsync();
|
try
|
||||||
if (!DoOnce())
|
|
||||||
{
|
{
|
||||||
return;
|
await _accountListOverlay.HideAsync();
|
||||||
|
_vm.MoreCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
var buttons = _email.IsEnabled ? new[] { AppResources.GetPasswordHint }
|
|
||||||
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
|
||||||
var selection = await DisplayActionSheet(AppResources.Options,
|
|
||||||
AppResources.Cancel, null, buttons);
|
|
||||||
|
|
||||||
if (selection == AppResources.GetPasswordHint)
|
|
||||||
{
|
{
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
_logger.Value.Exception(ex);
|
||||||
}
|
|
||||||
else if (selection == AppResources.RemoveAccount)
|
|
||||||
{
|
|
||||||
await _vm.RemoveAccountAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
@@ -8,6 +9,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -28,6 +30,7 @@ namespace Bit.App.Pages
|
|||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
|
private bool _isEmailEnabled;
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -44,6 +47,7 @@ namespace Bit.App.Pages
|
|||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
LogInCommand = new Command(async () => await LogInAsync());
|
LogInCommand = new Command(async () => await LogInAsync());
|
||||||
|
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
{
|
{
|
||||||
@@ -81,10 +85,19 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _masterPassword, value);
|
set => SetProperty(ref _masterPassword, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsEmailEnabled
|
||||||
|
{
|
||||||
|
get => _isEmailEnabled;
|
||||||
|
set => SetProperty(ref _isEmailEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIosExtension { get; set; }
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
public ICommand MoreCommand { get; internal set; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
@@ -201,6 +214,28 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task MoreAsync()
|
||||||
|
{
|
||||||
|
var buttons = IsEmailEnabled
|
||||||
|
? new[] { AppResources.GetPasswordHint }
|
||||||
|
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
||||||
|
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
||||||
|
|
||||||
|
if (selection == AppResources.GetPasswordHint)
|
||||||
|
{
|
||||||
|
var hintNavigationPage = new NavigationPage(new HintPage());
|
||||||
|
if (IsIosExtension)
|
||||||
|
{
|
||||||
|
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
||||||
|
}
|
||||||
|
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.RemoveAccount)
|
||||||
|
{
|
||||||
|
await RemoveAccountAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
|
|||||||
@@ -69,12 +69,12 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void LogIn_Clicked(object sender, EventArgs e)
|
private void LogIn_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
CopyAppOptions();
|
CopyAppOptions();
|
||||||
await _vm.LogInAsync();
|
_vm.LogInCommand.Execute(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -8,13 +9,15 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class LoginSsoPageViewModel : BaseViewModel
|
public class LoginSsoPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
|
private const string REDIRECT_URI = "bitwarden://sso-callback";
|
||||||
|
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
@@ -23,6 +26,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private string _orgIdentifier;
|
private string _orgIdentifier;
|
||||||
|
|
||||||
@@ -37,9 +41,11 @@ namespace Bit.App.Pages
|
|||||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
LogInCommand = new Command(async () => await LogInAsync());
|
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string OrgIdentifier
|
public string OrgIdentifier
|
||||||
@@ -48,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _orgIdentifier, value);
|
set => SetProperty(ref _orgIdentifier, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command LogInCommand { get; }
|
public ICommand LogInCommand { get; }
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action SsoAuthSuccessAction { get; set; }
|
public Action SsoAuthSuccessAction { get; set; }
|
||||||
@@ -65,78 +71,91 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task LogInAsync()
|
public async Task LogInAsync()
|
||||||
{
|
{
|
||||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
||||||
AppResources.InternetConnectionRequiredTitle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(
|
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
|
|
||||||
AppResources.AnErrorHasOccurred,
|
|
||||||
AppResources.Ok);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _apiService.PreValidateSso(OrgIdentifier);
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(
|
||||||
|
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
|
||||||
|
AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||||
|
|
||||||
|
var response = await _apiService.PreValidateSso(OrgIdentifier);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(response?.Token))
|
||||||
|
{
|
||||||
|
_logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace");
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssoToken = response.Token;
|
||||||
|
|
||||||
|
|
||||||
|
var passwordOptions = new PasswordGenerationOptions(true);
|
||||||
|
passwordOptions.Length = 64;
|
||||||
|
|
||||||
|
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||||
|
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||||
|
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
|
||||||
|
|
||||||
|
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||||
|
|
||||||
|
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
|
||||||
|
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
|
||||||
|
"redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" +
|
||||||
|
"response_type=code&scope=api%20offline_access&" +
|
||||||
|
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
||||||
|
"code_challenge_method=S256&response_mode=query&" +
|
||||||
|
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
|
||||||
|
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
||||||
|
|
||||||
|
WebAuthenticatorResult authResult = null;
|
||||||
|
|
||||||
|
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||||
|
new Uri(REDIRECT_URI));
|
||||||
|
|
||||||
|
|
||||||
|
var code = GetResultCode(authResult, state);
|
||||||
|
if (!string.IsNullOrEmpty(code))
|
||||||
|
{
|
||||||
|
await LogIn(code, codeVerifier, OrgIdentifier);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
{
|
{
|
||||||
|
_logger.Exception(e);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _platformUtilsService.ShowDialogAsync(
|
await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError,
|
||||||
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
|
|
||||||
AppResources.AnErrorHasOccurred);
|
AppResources.AnErrorHasOccurred);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwordOptions = new PasswordGenerationOptions(true);
|
|
||||||
passwordOptions.Length = 64;
|
|
||||||
|
|
||||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
|
||||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
|
||||||
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
|
|
||||||
|
|
||||||
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
|
||||||
|
|
||||||
var redirectUri = "bitwarden://sso-callback";
|
|
||||||
|
|
||||||
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
|
|
||||||
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
|
|
||||||
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
|
|
||||||
"response_type=code&scope=api%20offline_access&" +
|
|
||||||
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
|
||||||
"code_challenge_method=S256&response_mode=query&" +
|
|
||||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
|
|
||||||
|
|
||||||
WebAuthenticatorResult authResult = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
|
||||||
new Uri(redirectUri));
|
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
// user canceled
|
// user canceled
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
var code = GetResultCode(authResult, state);
|
|
||||||
if (!string.IsNullOrEmpty(code))
|
|
||||||
{
|
|
||||||
await LogIn(code, codeVerifier, redirectUri, OrgIdentifier);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +174,11 @@ namespace Bit.App.Pages
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogIn(string code, string codeVerifier, string redirectUri, string orgId)
|
private async Task LogIn(string code, string codeVerifier, string orgId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri, orgId);
|
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
IsToggled="{Binding AcceptPolicies}"
|
IsToggled="{Binding AcceptPolicies}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
Margin="{Binding SwitchMargin}"/>
|
Margin="0, 0, 10, 0"/>
|
||||||
<Label StyleClass="box-footer-label"
|
<Label StyleClass="box-footer-label"
|
||||||
HorizontalOptions="Fill">
|
HorizontalOptions="Fill">
|
||||||
<Label.FormattedText>
|
<Label.FormattedText>
|
||||||
|
|||||||
@@ -61,14 +61,6 @@ namespace Bit.App.Pages
|
|||||||
get => _acceptPolicies;
|
get => _acceptPolicies;
|
||||||
set => SetProperty(ref _acceptPolicies, value);
|
set => SetProperty(ref _acceptPolicies, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Thickness SwitchMargin
|
|
||||||
{
|
|
||||||
get => Device.RuntimePlatform == Device.Android
|
|
||||||
? new Thickness(0, 0, 0, 0)
|
|
||||||
: new Thickness(0, 0, 10, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowTerms { get; set; }
|
public bool ShowTerms { get; set; }
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ namespace Bit.App.Pages
|
|||||||
// Request
|
// Request
|
||||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||||
{
|
{
|
||||||
ResetPasswordKey = encryptedKey.EncryptedString
|
ResetPasswordKey = encryptedKey.EncryptedString,
|
||||||
|
MasterPasswordHash = masterPasswordHash,
|
||||||
};
|
};
|
||||||
var userId = await _stateService.GetActiveUserIdAsync();
|
var userId = await _stateService.GetActiveUserIdAsync();
|
||||||
// Enroll user
|
// Enroll user
|
||||||
|
|||||||
@@ -17,15 +17,19 @@
|
|||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
x:Name="_cancelItem" />
|
x:Name="_cancelItem" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
<u:IsNullConverter x:Key="isNull" />
|
<u:IsNullConverter x:Key="isNull" />
|
||||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
<ToolbarItem
|
||||||
x:Name="_moreItem" x:Key="moreItem"
|
x:Name="_moreItem"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
x:Key="moreItem"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
Icon="more_vert.png"
|
||||||
|
Order="Primary"
|
||||||
|
Command="{Binding MoreCommand}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||||
Clicked="Methods_Clicked"
|
Clicked="Methods_Clicked"
|
||||||
Order="Secondary"
|
Order="Secondary"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -137,21 +136,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void More_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (!DoOnce())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
|
||||||
|
|
||||||
if (selection == AppResources.UseAnotherTwoStepMethod)
|
|
||||||
{
|
|
||||||
await _vm.AnotherMethodAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -12,6 +13,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IAppIdService _appIdService;
|
private readonly IAppIdService _appIdService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private TwoFactorProviderType? _selectedProviderType;
|
private TwoFactorProviderType? _selectedProviderType;
|
||||||
private string _totpInstruction;
|
private string _totpInstruction;
|
||||||
@@ -51,9 +54,11 @@ namespace Bit.App.Pages
|
|||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
|
||||||
PageTitle = AppResources.TwoStepLogin;
|
PageTitle = AppResources.TwoStepLogin;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
|
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TotpInstruction
|
public string TotpInstruction
|
||||||
@@ -111,6 +116,7 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
|
public ICommand MoreCommand { get; }
|
||||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
@@ -337,6 +343,15 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task MoreAsync()
|
||||||
|
{
|
||||||
|
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
||||||
|
if (selection == AppResources.UseAnotherTwoStepMethod)
|
||||||
|
{
|
||||||
|
await AnotherMethodAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AnotherMethodAsync()
|
public async Task AnotherMethodAsync()
|
||||||
{
|
{
|
||||||
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
|
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
x:DataType="pages:GeneratorPageViewModel"
|
x:DataType="pages:GeneratorPageViewModel"
|
||||||
Title="{Binding PageTitle}">
|
Title="{Binding PageTitle}">
|
||||||
<ContentPage.BindingContext>
|
<ContentPage.BindingContext>
|
||||||
@@ -128,7 +129,11 @@
|
|||||||
Text="{Binding WordSeparator}"
|
Text="{Binding WordSeparator}"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value">
|
||||||
|
<Entry.Effects>
|
||||||
|
<effects:NoEmojiKeyboardEffect />
|
||||||
|
</Entry.Effects>
|
||||||
|
</Entry>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<pages:BaseContentPage
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
@@ -303,14 +303,14 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<controls:ExtendedDatePicker
|
<controls:ExtendedDatePicker
|
||||||
NullableDate="{Binding DeletionDate, Mode=TwoWay}"
|
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
|
||||||
Format="d"
|
Format="d"
|
||||||
IsEnabled="{Binding SendEnabled}"
|
IsEnabled="{Binding SendEnabled}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n DeletionDate}"
|
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:ExtendedTimePicker
|
<controls:ExtendedTimePicker
|
||||||
NullableTime="{Binding DeletionTime, Mode=TwoWay}"
|
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||||
Format="t"
|
Format="t"
|
||||||
IsEnabled="{Binding SendEnabled}"
|
IsEnabled="{Binding SendEnabled}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<controls:ExtendedDatePicker
|
<controls:ExtendedDatePicker
|
||||||
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
|
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
|
||||||
PlaceHolder="mm/dd/yyyy"
|
PlaceHolder="mm/dd/yyyy"
|
||||||
Format="d"
|
Format="d"
|
||||||
IsEnabled="{Binding SendEnabled}"
|
IsEnabled="{Binding SendEnabled}"
|
||||||
@@ -351,7 +351,7 @@
|
|||||||
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:ExtendedTimePicker
|
<controls:ExtendedTimePicker
|
||||||
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
|
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||||
PlaceHolder="--:-- --"
|
PlaceHolder="--:-- --"
|
||||||
Format="t"
|
Format="t"
|
||||||
IsEnabled="{Binding SendEnabled}"
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace Bit.App.Pages
|
|||||||
private AppOptions _appOptions;
|
private AppOptions _appOptions;
|
||||||
private SendAddEditPageViewModel _vm;
|
private SendAddEditPageViewModel _vm;
|
||||||
|
|
||||||
public Action OnClose { get; set; }
|
|
||||||
public Action AfterSubmit { get; set; }
|
public Action AfterSubmit { get; set; }
|
||||||
|
|
||||||
public SendAddEditPage(
|
public SendAddEditPage(
|
||||||
@@ -136,14 +135,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task CloseAsync()
|
private async Task CloseAsync()
|
||||||
{
|
{
|
||||||
if (OnClose is null)
|
await Navigation.PopModalAsync();
|
||||||
{
|
|
||||||
await Navigation.PopModalAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OnClose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendService _sendService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private bool _sendEnabled;
|
private bool _sendEnabled = true;
|
||||||
private bool _canAccessPremium;
|
private bool _canAccessPremium;
|
||||||
private bool _emailVerified;
|
private bool _emailVerified;
|
||||||
private SendView _send;
|
private SendView _send;
|
||||||
@@ -33,11 +34,7 @@ namespace Bit.App.Pages
|
|||||||
private int _deletionDateTypeSelectedIndex;
|
private int _deletionDateTypeSelectedIndex;
|
||||||
private int _expirationDateTypeSelectedIndex;
|
private int _expirationDateTypeSelectedIndex;
|
||||||
private DateTime _simpleDeletionDateTime;
|
private DateTime _simpleDeletionDateTime;
|
||||||
private DateTime _deletionDate;
|
|
||||||
private TimeSpan _deletionTime;
|
|
||||||
private DateTime? _simpleExpirationDateTime;
|
private DateTime? _simpleExpirationDateTime;
|
||||||
private DateTime? _expirationDate;
|
|
||||||
private TimeSpan? _expirationTime;
|
|
||||||
private bool _isOverridingPickers;
|
private bool _isOverridingPickers;
|
||||||
private int? _maxAccessCount;
|
private int? _maxAccessCount;
|
||||||
private string[] _additionalSendProperties = new[]
|
private string[] _additionalSendProperties = new[]
|
||||||
@@ -89,8 +86,34 @@ namespace Bit.App.Pages
|
|||||||
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||||
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime);
|
||||||
|
ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime)
|
||||||
|
{
|
||||||
|
OnDateChanged = date =>
|
||||||
|
{
|
||||||
|
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue)
|
||||||
|
{
|
||||||
|
// auto-set time to current time upon setting date
|
||||||
|
ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OnTimeChanged = time =>
|
||||||
|
{
|
||||||
|
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue)
|
||||||
|
{
|
||||||
|
// auto-set date to current date upon setting time
|
||||||
|
ExpirationDateTimeViewModel.Date = DateTime.Today;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DatePlaceholder = "mm/dd/yyyy",
|
||||||
|
TimePlaceholder = "--:-- --"
|
||||||
|
};
|
||||||
|
|
||||||
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
public Command TogglePasswordCommand { get; set; }
|
public Command TogglePasswordCommand { get; set; }
|
||||||
public Command ToggleOptionsCommand { get; set; }
|
public Command ToggleOptionsCommand { get; set; }
|
||||||
public string SendId { get; set; }
|
public string SendId { get; set; }
|
||||||
@@ -126,23 +149,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public DateTime DeletionDate
|
|
||||||
{
|
|
||||||
get => _deletionDate;
|
|
||||||
set => SetProperty(ref _deletionDate, value);
|
|
||||||
}
|
|
||||||
public TimeSpan DeletionTime
|
|
||||||
{
|
|
||||||
get => _deletionTime;
|
|
||||||
set => SetProperty(ref _deletionTime, value);
|
|
||||||
}
|
|
||||||
public bool ShowOptions
|
public bool ShowOptions
|
||||||
{
|
{
|
||||||
get => _showOptions;
|
get => _showOptions;
|
||||||
set => SetProperty(ref _showOptions, value,
|
set => SetProperty(ref _showOptions, value,
|
||||||
additionalPropertyNames: new[]
|
additionalPropertyNames: new[]
|
||||||
{
|
{
|
||||||
nameof(OptionsAccessilibityText)
|
nameof(OptionsAccessilibityText),
|
||||||
|
nameof(OptionsShowHideIcon)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public int ExpirationDateTypeSelectedIndex
|
public int ExpirationDateTypeSelectedIndex
|
||||||
@@ -156,28 +170,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public DateTime? ExpirationDate
|
|
||||||
{
|
|
||||||
get => _expirationDate;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _expirationDate, value))
|
|
||||||
{
|
|
||||||
ExpirationDateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public TimeSpan? ExpirationTime
|
|
||||||
{
|
|
||||||
get => _expirationTime;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _expirationTime, value))
|
|
||||||
{
|
|
||||||
ExpirationTimeChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public int? MaxAccessCount
|
public int? MaxAccessCount
|
||||||
{
|
{
|
||||||
get => _maxAccessCount;
|
get => _maxAccessCount;
|
||||||
@@ -205,7 +198,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
public string FileName
|
public string FileName
|
||||||
{
|
{
|
||||||
get => _fileName;
|
get => _fileName ?? AppResources.NoFileChosen;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _fileName, value))
|
if (SetProperty(ref _fileName, value))
|
||||||
@@ -240,10 +233,13 @@ namespace Bit.App.Pages
|
|||||||
public bool IsFile => Send?.Type == SendType.File;
|
public bool IsFile => Send?.Type == SendType.File;
|
||||||
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
|
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
|
||||||
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
|
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
|
||||||
|
public DateTimeViewModel DeletionDateTimeViewModel { get; }
|
||||||
|
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
|
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
|
||||||
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
|
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
|
||||||
|
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
@@ -268,10 +264,8 @@ namespace Bit.App.Pages
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Send = await send.DecryptAsync();
|
Send = await send.DecryptAsync();
|
||||||
DeletionDate = Send.DeletionDate.ToLocalTime();
|
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
|
||||||
DeletionTime = DeletionDate.TimeOfDay;
|
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
|
||||||
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
|
|
||||||
ExpirationTime = ExpirationDate?.TimeOfDay;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -280,8 +274,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
Type = Type.GetValueOrDefault(defaultType),
|
Type = Type.GetValueOrDefault(defaultType),
|
||||||
};
|
};
|
||||||
_deletionDate = DateTimeNow().AddDays(7);
|
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
|
||||||
_deletionTime = DeletionDate.TimeOfDay;
|
|
||||||
DeletionDateTypeSelectedIndex = 4;
|
DeletionDateTypeSelectedIndex = 4;
|
||||||
ExpirationDateTypeSelectedIndex = 0;
|
ExpirationDateTypeSelectedIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -305,23 +298,22 @@ namespace Bit.App.Pages
|
|||||||
public void ClearExpirationDate()
|
public void ClearExpirationDate()
|
||||||
{
|
{
|
||||||
_isOverridingPickers = true;
|
_isOverridingPickers = true;
|
||||||
ExpirationDate = null;
|
ExpirationDateTimeViewModel.DateTime = null;
|
||||||
ExpirationTime = null;
|
|
||||||
_isOverridingPickers = false;
|
_isOverridingPickers = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSendData()
|
private void UpdateSendData()
|
||||||
{
|
{
|
||||||
// filename
|
// filename
|
||||||
if (Send.File != null && FileName != null)
|
if (Send.File != null && _fileName != null)
|
||||||
{
|
{
|
||||||
Send.File.FileName = FileName;
|
Send.File.FileName = _fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// deletion date
|
// deletion date
|
||||||
if (ShowDeletionCustomPickers)
|
if (ShowDeletionCustomPickers)
|
||||||
{
|
{
|
||||||
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
|
Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -329,9 +321,9 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expiration date
|
// expiration date
|
||||||
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue)
|
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
|
||||||
{
|
{
|
||||||
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
|
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||||
}
|
}
|
||||||
else if (_simpleExpirationDateTime.HasValue)
|
else if (_simpleExpirationDateTime.HasValue)
|
||||||
{
|
{
|
||||||
@@ -484,7 +476,7 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null)
|
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
|
||||||
{
|
{
|
||||||
sendPage.OnClose();
|
sendPage.OnClose();
|
||||||
return;
|
return;
|
||||||
@@ -625,24 +617,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExpirationDateChanged()
|
|
||||||
{
|
|
||||||
if (!_isOverridingPickers && !ExpirationTime.HasValue)
|
|
||||||
{
|
|
||||||
// auto-set time to current time upon setting date
|
|
||||||
ExpirationTime = DateTimeNow().TimeOfDay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExpirationTimeChanged()
|
|
||||||
{
|
|
||||||
if (!_isOverridingPickers && !ExpirationDate.HasValue)
|
|
||||||
{
|
|
||||||
// auto-set date to current date upon setting time
|
|
||||||
ExpirationDate = DateTime.Today;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaxAccessCountChanged()
|
private void MaxAccessCountChanged()
|
||||||
{
|
{
|
||||||
Send.MaxAccessCount = _maxAccessCount;
|
Send.MaxAccessCount = _maxAccessCount;
|
||||||
@@ -666,5 +640,10 @@ namespace Bit.App.Pages
|
|||||||
DateTimeKind.Local
|
DateTimeKind.Local
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void TriggerSendTextPropertyChanged()
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
183
src/App/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
183
src/App/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<ContentView
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
|
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
|
||||||
|
x:DataType="pages:SendAddEditPageViewModel"
|
||||||
|
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
|
||||||
|
<ContentView.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentView.Resources>
|
||||||
|
<ContentView.Content>
|
||||||
|
<StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeletionDate}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_deletionDateTypePicker"
|
||||||
|
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
ItemDisplayBinding="{Binding Key}"
|
||||||
|
ios:Picker.UpdateMode="WhenFinished"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n DeletionTime}" />
|
||||||
|
<controls:LazyDateTimePicker
|
||||||
|
x:Name="_lazyDeletionDateTimePicker"
|
||||||
|
BindingContext="{Binding DeletionDateTimeViewModel}"
|
||||||
|
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeletionDateInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ExpirationDate}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_expirationDateTypePicker"
|
||||||
|
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
|
||||||
|
ItemDisplayBinding="{Binding Key}"
|
||||||
|
ios:Picker.UpdateMode="WhenFinished"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ExpirationTime}" />
|
||||||
|
<controls:LazyDateTimePicker
|
||||||
|
x:Name="_lazyExpirationDateTimePicker"
|
||||||
|
BindingContext="{Binding ExpirationDateTimeViewModel}"
|
||||||
|
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ExpirationDateInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
HorizontalOptions="StartAndExpand"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n MaximumAccessCount}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<Entry
|
||||||
|
Text="{Binding MaxAccessCount}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Keyboard="Numeric"
|
||||||
|
MaxLength="9"
|
||||||
|
TextChanged="OnMaxAccessCountTextChanged"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
<controls:ExtendedStepper
|
||||||
|
x:Name="_maxAccessCountStepper"
|
||||||
|
Value="{Binding MaxAccessCount}"
|
||||||
|
Maximum="999999999"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n MaximumAccessCountInfo}"
|
||||||
|
StyleClass="box-footer-label" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NewPassword}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<StackLayout Orientation="Horizontal">
|
||||||
|
<Entry
|
||||||
|
Text="{Binding NewPassword}"
|
||||||
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
IsSpellCheckEnabled="False"
|
||||||
|
IsTextPredictionEnabled="False"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
|
<controls:IconButton
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding ShowPasswordIcon}"
|
||||||
|
Command="{Binding TogglePasswordCommand}"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n PasswordInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Notes}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Editor
|
||||||
|
x:Name="_notesEditor"
|
||||||
|
AutoSize="TextChanges"
|
||||||
|
Text="{Binding Send.Notes}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Margin="0,10,0,5"
|
||||||
|
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||||
|
<Editor.Effects>
|
||||||
|
<effects:ScrollEnabledEffect />
|
||||||
|
</Editor.Effects>
|
||||||
|
</Editor>
|
||||||
|
<BoxView
|
||||||
|
StyleClass="box-row-separator" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NotesInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row, box-row-switch"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n HideEmail}"
|
||||||
|
StyleClass="box-label-regular"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding Send.HideEmail}"
|
||||||
|
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row, box-row-switch"
|
||||||
|
Margin="0,5,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DisableSend}"
|
||||||
|
StyleClass="box-label-regular"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding Send.Disabled}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
</ContentView.Content>
|
||||||
|
</ContentView>
|
||||||
91
src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
91
src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Behaviors;
|
||||||
|
using Xamarin.CommunityToolkit.UI.Views;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class SendAddOnlyOptionsView : ContentView
|
||||||
|
{
|
||||||
|
public SendAddOnlyOptionsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
|
||||||
|
|
||||||
|
public void SetMainScrollView(ScrollView scrollView)
|
||||||
|
{
|
||||||
|
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(e.NewTextValue))
|
||||||
|
{
|
||||||
|
ViewModel.MaxAccessCount = null;
|
||||||
|
_maxAccessCountStepper.Value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// accept only digits
|
||||||
|
if (!int.TryParse(e.NewTextValue, out int _))
|
||||||
|
{
|
||||||
|
((Entry)sender).Text = e.OldTextValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
if (propertyName == nameof(BindingContext)
|
||||||
|
&&
|
||||||
|
ViewModel != null)
|
||||||
|
{
|
||||||
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_lazyDeletionDateTimePicker.IsLoaded
|
||||||
|
&&
|
||||||
|
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
|
||||||
|
&&
|
||||||
|
ViewModel.ShowDeletionCustomPickers)
|
||||||
|
{
|
||||||
|
_lazyDeletionDateTimePicker.LoadViewAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_lazyExpirationDateTimePicker.IsLoaded
|
||||||
|
&&
|
||||||
|
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
|
||||||
|
&&
|
||||||
|
ViewModel.ShowExpirationCustomPickers)
|
||||||
|
{
|
||||||
|
_lazyExpirationDateTimePicker.LoadViewAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
|
||||||
|
{
|
||||||
|
public ScrollView MainScrollView { get; set; }
|
||||||
|
|
||||||
|
public override async ValueTask LoadViewAsync()
|
||||||
|
{
|
||||||
|
await base.LoadViewAsync();
|
||||||
|
|
||||||
|
if (Content is SendAddOnlyOptionsView optionsView)
|
||||||
|
{
|
||||||
|
optionsView.SetMainScrollView(MainScrollView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/App/Pages/Send/SendAddOnlyPage.xaml
Normal file
190
src/App/Pages/Send/SendAddOnlyPage.xaml
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.SendAddOnlyPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||||
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
|
x:DataType="pages:SendAddEditPageViewModel"
|
||||||
|
x:Name="_page"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:SendAddEditPageViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.ToolbarItems>
|
||||||
|
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
|
||||||
|
<controls:ExtendedToolbarItem
|
||||||
|
x:Name="_accountAvatar"
|
||||||
|
IconImageSource="{Binding AvatarImageSource}"
|
||||||
|
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||||
|
Order="Primary"
|
||||||
|
Priority="-2"
|
||||||
|
UseOriginalImage="True"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Account}" />
|
||||||
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||||
|
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||||
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
|
||||||
|
<AbsoluteLayout>
|
||||||
|
<ScrollView
|
||||||
|
x:Name="_scrollView"
|
||||||
|
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||||
|
AbsoluteLayout.LayoutFlags="All">
|
||||||
|
<StackLayout x:Name="_mainContainer" StyleClass="box">
|
||||||
|
<Frame
|
||||||
|
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
|
||||||
|
Padding="10"
|
||||||
|
Margin="0, 12, 0, 0"
|
||||||
|
HasShadow="False"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
BorderColor="Accent">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n SendDisabledWarning}"
|
||||||
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
</Frame>
|
||||||
|
<Frame
|
||||||
|
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||||
|
Padding="10"
|
||||||
|
Margin="0, 12, 0, 0"
|
||||||
|
HasShadow="False"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
BorderColor="Accent">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||||
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
</Frame>
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Name}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Entry
|
||||||
|
x:Name="_nameEntry"
|
||||||
|
Text="{Binding Send.Name}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NameInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Margin="0,5,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
IsVisible="{Binding IsFile}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n TypeFile}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{Binding FileName}"
|
||||||
|
LineBreakMode="CharacterWrap"
|
||||||
|
StyleClass="text-sm, text-muted"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Label
|
||||||
|
Margin="0, 5, 0, 0"
|
||||||
|
Text="{u:I18n MaxFileSize}"
|
||||||
|
StyleClass="text-sm, text-muted"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row"
|
||||||
|
IsVisible="{Binding IsText}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n TypeText}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Editor
|
||||||
|
x:Name="_textEditor"
|
||||||
|
AutoSize="TextChanges"
|
||||||
|
Text="{Binding Send.Text.Text}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Margin="{Binding EditorMargins}"
|
||||||
|
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||||
|
<Editor.Behaviors>
|
||||||
|
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||||
|
</Editor.Behaviors>
|
||||||
|
<Editor.Effects>
|
||||||
|
<effects:ScrollEnabledEffect />
|
||||||
|
</Editor.Effects>
|
||||||
|
</Editor>
|
||||||
|
<BoxView
|
||||||
|
StyleClass="box-row-separator" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n TypeTextInfo}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Margin="0,5,0,10" />
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row, box-row-switch"
|
||||||
|
Margin="0,10,0,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n HideTextByDefault}"
|
||||||
|
StyleClass="box-label-regular"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding Send.Text.Hidden}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row, box-row-switch">
|
||||||
|
<Label
|
||||||
|
Text="{Binding ShareOnSaveText}"
|
||||||
|
StyleClass="box-label-regular"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalOptions="StartAndExpand" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding ShareOnSave}"
|
||||||
|
IsEnabled="{Binding SendEnabled}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="0"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
|
||||||
|
<StackLayout.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
|
||||||
|
</StackLayout.GestureRecognizers>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Options}"
|
||||||
|
TextColor="{DynamicResource PrimaryColor}"
|
||||||
|
Margin="0,0,5,0"
|
||||||
|
AutomationProperties.IsInAccessibleTree="False"/>
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding OptionsShowHideIcon}"
|
||||||
|
TextColor="{DynamicResource PrimaryColor}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="False"/>
|
||||||
|
</StackLayout>
|
||||||
|
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<controls:AccountSwitchingOverlayView
|
||||||
|
x:Name="_accountListOverlay"
|
||||||
|
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||||
|
AbsoluteLayout.LayoutFlags="All"
|
||||||
|
LongPressAccountEnabled="False"
|
||||||
|
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||||
|
</AbsoluteLayout>
|
||||||
|
|
||||||
|
</pages:BaseContentPage>
|
||||||
178
src/App/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
178
src/App/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
|
||||||
|
/// for performance for iOS Share extension.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This should NOT be used in Android.
|
||||||
|
/// </remarks>
|
||||||
|
public partial class SendAddOnlyPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
|
private AppOptions _appOptions;
|
||||||
|
private SendAddEditPageViewModel _vm;
|
||||||
|
|
||||||
|
public Action OnClose { get; set; }
|
||||||
|
public Action AfterSubmit { get; set; }
|
||||||
|
|
||||||
|
public SendAddOnlyPage(
|
||||||
|
AppOptions appOptions = null,
|
||||||
|
string sendId = null,
|
||||||
|
SendType? type = null)
|
||||||
|
{
|
||||||
|
if (appOptions?.IosExtension != true)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
_appOptions = appOptions;
|
||||||
|
InitializeComponent();
|
||||||
|
_vm = BindingContext as SendAddEditPageViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
_vm.SendId = sendId;
|
||||||
|
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
|
||||||
|
|
||||||
|
if (_vm.IsText)
|
||||||
|
{
|
||||||
|
_nameEntry.ReturnType = ReturnType.Next;
|
||||||
|
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||||
|
{
|
||||||
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
|
}
|
||||||
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _vm.InitAsync();
|
||||||
|
|
||||||
|
if (!await _vm.LoadAsync())
|
||||||
|
{
|
||||||
|
await CloseAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_accountAvatar?.OnAppearing();
|
||||||
|
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
|
||||||
|
|
||||||
|
await HandleCreateRequest();
|
||||||
|
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||||
|
{
|
||||||
|
RequestFocus(_nameEntry);
|
||||||
|
}
|
||||||
|
AdjustToolbar();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
await CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
_accountAvatar?.OnDisappearing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseAsync()
|
||||||
|
{
|
||||||
|
if (OnClose is null)
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Save_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
var submitted = await _vm.SubmitAsync();
|
||||||
|
if (submitted)
|
||||||
|
{
|
||||||
|
AfterSubmit?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Close_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustToolbar()
|
||||||
|
{
|
||||||
|
_saveItem.IsEnabled = _vm.SendEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleCreateRequest()
|
||||||
|
{
|
||||||
|
if (_appOptions?.CreateSend == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vm.IsAddFromShare = true;
|
||||||
|
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
|
||||||
|
|
||||||
|
var name = _appOptions.CreateSend.Item2;
|
||||||
|
_vm.Send.Name = name;
|
||||||
|
|
||||||
|
var type = _appOptions.CreateSend.Item1;
|
||||||
|
if (type == SendType.File)
|
||||||
|
{
|
||||||
|
_vm.FileData = _appOptions.CreateSend.Item3;
|
||||||
|
_vm.FileName = name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var text = _appOptions.CreateSend.Item4;
|
||||||
|
_vm.Send.Text.Text = text;
|
||||||
|
_vm.TriggerSendTextPropertyChanged();
|
||||||
|
}
|
||||||
|
_appOptions.CreateSend = null;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OptionsHeader_Tapped(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_vm.ToggleOptionsCommand.Execute(null);
|
||||||
|
|
||||||
|
if (!_lazyOptionsView.IsLoaded)
|
||||||
|
{
|
||||||
|
_lazyOptionsView.MainScrollView = _scrollView;
|
||||||
|
_lazyOptionsView.LoadViewAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using Bit.App.Controls;
|
|||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -68,21 +69,28 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "syncStarted")
|
try
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
if (message.Command == "syncStarted")
|
||||||
}
|
|
||||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
|
||||||
{
|
|
||||||
await Task.Delay(500);
|
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||||
if (_vm.LoadedOnce)
|
}
|
||||||
|
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
var task = _vm.LoadAsync();
|
IsBusy = false;
|
||||||
}
|
if (_vm.LoadedOnce)
|
||||||
});
|
{
|
||||||
|
var task = _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,23 @@
|
|||||||
StyleClass="box-footer-label"
|
StyleClass="box-footer-label"
|
||||||
Text="{u:I18n ThemeDescription}" />
|
Text="{u:I18n ThemeDescription}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box"
|
||||||
|
IsVisible="{Binding ShowAutoDarkThemeOptions}">
|
||||||
|
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DefaultDarkTheme}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Picker
|
||||||
|
x:Name="_autoDarkThemePicker"
|
||||||
|
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
Text="{u:I18n DefaultDarkThemeDescription}" />
|
||||||
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||||
<Label
|
<Label
|
||||||
@@ -66,31 +83,31 @@
|
|||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableAutoTotpCopy}"
|
Text="{u:I18n CopyTotpAutomatically}"
|
||||||
StyleClass="box-label-regular"
|
StyleClass="box-label-regular"
|
||||||
HorizontalOptions="StartAndExpand" />
|
HorizontalOptions="StartAndExpand" />
|
||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding DisableAutoTotpCopy}"
|
IsToggled="{Binding AutoTotpCopy}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableAutoTotpCopyDescription}"
|
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableWebsiteIcons}"
|
Text="{u:I18n ShowWebsiteIcons}"
|
||||||
StyleClass="box-label-regular"
|
StyleClass="box-label-regular"
|
||||||
HorizontalOptions="StartAndExpand" />
|
HorizontalOptions="StartAndExpand" />
|
||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding DisableFavicon}"
|
IsToggled="{Binding Favicon}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableWebsiteIconsDescription}"
|
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||||
@@ -100,35 +117,35 @@
|
|||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableSavePrompt}"
|
Text="{u:I18n AskToAddLogin}"
|
||||||
StyleClass="box-label-regular"
|
StyleClass="box-label-regular"
|
||||||
HorizontalOptions="StartAndExpand" />
|
HorizontalOptions="StartAndExpand" />
|
||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding AutofillDisableSavePrompt}"
|
IsToggled="{Binding AutofillSavePrompt}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DisableSavePromptDescription}"
|
Text="{u:I18n AskToAddLoginDescription}"
|
||||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||||
<StackLayout StyleClass="box-row, box-row-input">
|
<StackLayout StyleClass="box-row, box-row-input">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n BlacklistedUris}"
|
Text="{u:I18n AutofillBlockedUris}"
|
||||||
StyleClass="box-label" />
|
StyleClass="box-label" />
|
||||||
<Editor
|
<Editor
|
||||||
x:Name="_blacklistedUrisEditor"
|
x:Name="_autofillBlockedUrisEditor"
|
||||||
Text="{Binding AutofillBlacklistedUris}"
|
Text="{Binding AutofillBlockedUris}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
AutoSize="TextChanges"
|
AutoSize="TextChanges"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
Keyboard="Url"
|
Keyboard="Url"
|
||||||
Unfocused="BlacklistedUrisEditor_Unfocused" />
|
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n BlacklistedUrisDescription}"
|
Text="{u:I18n AutofillBlockedUrisDescription}"
|
||||||
StyleClass="box-footer-label" />
|
StyleClass="box-footer-label" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Bit.App.Pages
|
|||||||
_vm = BindingContext as OptionsPageViewModel;
|
_vm = BindingContext as OptionsPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_themePicker.ItemDisplayBinding = new Binding("Value");
|
_themePicker.ItemDisplayBinding = new Binding("Value");
|
||||||
|
_autoDarkThemePicker.ItemDisplayBinding = new Binding("Value");
|
||||||
_uriMatchPicker.ItemDisplayBinding = new Binding("Value");
|
_uriMatchPicker.ItemDisplayBinding = new Binding("Value");
|
||||||
_clearClipboardPicker.ItemDisplayBinding = new Binding("Value");
|
_clearClipboardPicker.ItemDisplayBinding = new Binding("Value");
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
|
_autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||||
}
|
}
|
||||||
@@ -43,12 +45,12 @@ namespace Bit.App.Pages
|
|||||||
protected async override void OnDisappearing()
|
protected async override void OnDisappearing()
|
||||||
{
|
{
|
||||||
base.OnDisappearing();
|
base.OnDisappearing();
|
||||||
await _vm.UpdateAutofillBlacklistedUris();
|
await _vm.UpdateAutofillBlockedUris();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void BlacklistedUrisEditor_Unfocused(object sender, FocusEventArgs e)
|
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
|
||||||
{
|
{
|
||||||
await _vm.UpdateAutofillBlacklistedUris();
|
await _vm.UpdateAutofillBlockedUris();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
public class OptionsPageViewModel : BaseViewModel
|
public class OptionsPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly ITotpService _totpService;
|
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
|
|
||||||
private bool _autofillDisableSavePrompt;
|
private bool _autofillSavePrompt;
|
||||||
private string _autofillBlacklistedUris;
|
private string _autofillBlockedUris;
|
||||||
private bool _disableFavicon;
|
private bool _favicon;
|
||||||
private bool _disableAutoTotpCopy;
|
private bool _autoTotpCopy;
|
||||||
private int _clearClipboardSelectedIndex;
|
private int _clearClipboardSelectedIndex;
|
||||||
private int _themeSelectedIndex;
|
private int _themeSelectedIndex;
|
||||||
|
private int _autoDarkThemeSelectedIndex;
|
||||||
private int _uriMatchSelectedIndex;
|
private int _uriMatchSelectedIndex;
|
||||||
private bool _inited;
|
private bool _inited;
|
||||||
private bool _updatingAutofill;
|
private bool _updatingAutofill;
|
||||||
@@ -30,7 +30,6 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public OptionsPageViewModel()
|
public OptionsPageViewModel()
|
||||||
{
|
{
|
||||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
|
|
||||||
@@ -53,10 +52,16 @@ namespace Bit.App.Pages
|
|||||||
ThemeOptions = new List<KeyValuePair<string, string>>
|
ThemeOptions = new List<KeyValuePair<string, string>>
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
|
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
|
||||||
new KeyValuePair<string, string>("light", AppResources.Light),
|
new KeyValuePair<string, string>(ThemeManager.Light, AppResources.Light),
|
||||||
new KeyValuePair<string, string>("dark", AppResources.Dark),
|
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||||
new KeyValuePair<string, string>("black", AppResources.Black),
|
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||||
new KeyValuePair<string, string>("nord", "Nord"),
|
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||||
|
};
|
||||||
|
AutoDarkThemeOptions = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||||
|
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||||
|
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||||
};
|
};
|
||||||
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
||||||
{
|
{
|
||||||
@@ -71,6 +76,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
|
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
|
||||||
public List<KeyValuePair<string, string>> ThemeOptions { get; set; }
|
public List<KeyValuePair<string, string>> ThemeOptions { get; set; }
|
||||||
|
public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; }
|
||||||
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
|
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
|
||||||
|
|
||||||
public int ClearClipboardSelectedIndex
|
public int ClearClipboardSelectedIndex
|
||||||
@@ -80,7 +86,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (SetProperty(ref _clearClipboardSelectedIndex, value))
|
if (SetProperty(ref _clearClipboardSelectedIndex, value))
|
||||||
{
|
{
|
||||||
var task = SaveClipboardChangedAsync();
|
SaveClipboardChangedAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,9 +96,25 @@ namespace Bit.App.Pages
|
|||||||
get => _themeSelectedIndex;
|
get => _themeSelectedIndex;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _themeSelectedIndex, value))
|
if (SetProperty(ref _themeSelectedIndex, value,
|
||||||
|
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var task = SaveThemeAsync();
|
SaveThemeAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
|
||||||
|
|
||||||
|
public int AutoDarkThemeSelectedIndex
|
||||||
|
{
|
||||||
|
get => _autoDarkThemeSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
|
||||||
|
{
|
||||||
|
SaveThemeAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,51 +126,51 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (SetProperty(ref _uriMatchSelectedIndex, value))
|
if (SetProperty(ref _uriMatchSelectedIndex, value))
|
||||||
{
|
{
|
||||||
var task = SaveDefaultUriAsync();
|
SaveDefaultUriAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DisableFavicon
|
public bool Favicon
|
||||||
{
|
{
|
||||||
get => _disableFavicon;
|
get => _favicon;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _disableFavicon, value))
|
if (SetProperty(ref _favicon, value))
|
||||||
{
|
{
|
||||||
var task = UpdateDisableFaviconAsync();
|
UpdateFaviconAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool DisableAutoTotpCopy
|
public bool AutoTotpCopy
|
||||||
{
|
{
|
||||||
get => _disableAutoTotpCopy;
|
get => _autoTotpCopy;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _disableAutoTotpCopy, value))
|
if (SetProperty(ref _autoTotpCopy, value))
|
||||||
{
|
{
|
||||||
var task = UpdateAutoTotpCopyAsync();
|
UpdateAutoTotpCopyAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AutofillDisableSavePrompt
|
public bool AutofillSavePrompt
|
||||||
{
|
{
|
||||||
get => _autofillDisableSavePrompt;
|
get => _autofillSavePrompt;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _autofillDisableSavePrompt, value))
|
if (SetProperty(ref _autofillSavePrompt, value))
|
||||||
{
|
{
|
||||||
var task = UpdateAutofillDisableSavePromptAsync();
|
UpdateAutofillSavePromptAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string AutofillBlacklistedUris
|
public string AutofillBlockedUris
|
||||||
{
|
{
|
||||||
get => _autofillBlacklistedUris;
|
get => _autofillBlockedUris;
|
||||||
set => SetProperty(ref _autofillBlacklistedUris, value);
|
set => SetProperty(ref _autofillBlockedUris, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShowAndroidAutofillSettings
|
public bool ShowAndroidAutofillSettings
|
||||||
@@ -159,13 +181,15 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
AutofillDisableSavePrompt = (await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||||
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||||
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
|
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
|
||||||
DisableAutoTotpCopy = !(await _totpService.IsAutoCopyEnabledAsync());
|
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
|
||||||
DisableFavicon = (await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||||
var theme = await _stateService.GetThemeAsync();
|
var theme = await _stateService.GetThemeAsync();
|
||||||
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
|
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
|
||||||
|
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
|
||||||
|
AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme);
|
||||||
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
|
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
|
||||||
UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
|
UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
|
||||||
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
|
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
|
||||||
@@ -178,15 +202,17 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (_inited)
|
if (_inited)
|
||||||
{
|
{
|
||||||
await _stateService.SetDisableAutoTotpCopyAsync(DisableAutoTotpCopy);
|
// TODO: [PS-961] Fix negative function names
|
||||||
|
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateDisableFaviconAsync()
|
private async Task UpdateFaviconAsync()
|
||||||
{
|
{
|
||||||
if (_inited)
|
if (_inited)
|
||||||
{
|
{
|
||||||
await _stateService.SetDisableFaviconAsync(DisableFavicon);
|
// TODO: [PS-961] Fix negative function names
|
||||||
|
await _stateService.SetDisableFaviconAsync(!Favicon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +228,8 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (_inited && ThemeSelectedIndex > -1)
|
if (_inited && ThemeSelectedIndex > -1)
|
||||||
{
|
{
|
||||||
var theme = ThemeOptions[ThemeSelectedIndex].Key;
|
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
|
||||||
await _stateService.SetThemeAsync(theme);
|
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
|
||||||
ThemeManager.SetTheme(Application.Current.Resources);
|
ThemeManager.SetTheme(Application.Current.Resources);
|
||||||
_messagingService.Send("updatedTheme");
|
_messagingService.Send("updatedTheme");
|
||||||
}
|
}
|
||||||
@@ -217,27 +243,28 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateAutofillDisableSavePromptAsync()
|
private async Task UpdateAutofillSavePromptAsync()
|
||||||
{
|
{
|
||||||
if (_inited)
|
if (_inited)
|
||||||
{
|
{
|
||||||
await _stateService.SetAutofillDisableSavePromptAsync(AutofillDisableSavePrompt);
|
// TODO: [PS-961] Fix negative function names
|
||||||
|
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAutofillBlacklistedUris()
|
public async Task UpdateAutofillBlockedUris()
|
||||||
{
|
{
|
||||||
if (_inited)
|
if (_inited)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(AutofillBlacklistedUris))
|
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
|
||||||
{
|
{
|
||||||
await _stateService.SetAutofillBlacklistedUrisAsync(null);
|
await _stateService.SetAutofillBlacklistedUrisAsync(null);
|
||||||
AutofillBlacklistedUris = null;
|
AutofillBlockedUris = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var csv = AutofillBlacklistedUris;
|
var csv = AutofillBlockedUris;
|
||||||
var urisList = new List<string>();
|
var urisList = new List<string>();
|
||||||
foreach (var uri in csv.Split(','))
|
foreach (var uri in csv.Split(','))
|
||||||
{
|
{
|
||||||
@@ -254,7 +281,7 @@ namespace Bit.App.Pages
|
|||||||
urisList.Add(cleanedUri);
|
urisList.Add(cleanedUri);
|
||||||
}
|
}
|
||||||
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
|
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
|
||||||
AutofillBlacklistedUris = string.Join(", ", urisList);
|
AutofillBlockedUris = string.Join(", ", urisList);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,13 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Pages.Accounts;
|
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class SettingsPage : BaseContentPage
|
public partial class SettingsPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
|
||||||
private readonly TabsPage _tabsPage;
|
private readonly TabsPage _tabsPage;
|
||||||
private SettingsPageViewModel _vm;
|
private SettingsPageViewModel _vm;
|
||||||
|
|
||||||
@@ -21,7 +16,6 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_tabsPage = tabsPage;
|
_tabsPage = tabsPage;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
||||||
_vm = BindingContext as SettingsPageViewModel;
|
_vm = BindingContext as SettingsPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
}
|
}
|
||||||
@@ -67,122 +61,12 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
private void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||||
if (!DoOnce())
|
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
|
||||||
{
|
{
|
||||||
return;
|
_vm?.ExecuteSettingItemCommand.Execute(item);
|
||||||
}
|
|
||||||
if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Name == AppResources.Sync)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new SyncPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.AutofillServices)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(this)));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.PasswordAutofill)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new AutofillPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.AppExtension)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.Options)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.Folders)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new FoldersPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.About)
|
|
||||||
{
|
|
||||||
await _vm.AboutAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.HelpAndFeedback)
|
|
||||||
{
|
|
||||||
_vm.Help();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.FingerprintPhrase)
|
|
||||||
{
|
|
||||||
await _vm.FingerprintAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.RateTheApp)
|
|
||||||
{
|
|
||||||
_vm.Rate();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.ImportItems)
|
|
||||||
{
|
|
||||||
_vm.Import();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.ExportVault)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.LearnOrg)
|
|
||||||
{
|
|
||||||
await _vm.ShareAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.WebVault)
|
|
||||||
{
|
|
||||||
_vm.WebVault();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.ChangeMasterPassword)
|
|
||||||
{
|
|
||||||
await _vm.ChangePasswordAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.TwoStepLogin)
|
|
||||||
{
|
|
||||||
await _vm.TwoStepAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.LogOut)
|
|
||||||
{
|
|
||||||
await _vm.LogOutAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.DeleteAccount)
|
|
||||||
{
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.LockNow)
|
|
||||||
{
|
|
||||||
await _vm.LockAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.VaultTimeout)
|
|
||||||
{
|
|
||||||
await _vm.VaultTimeoutAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.VaultTimeoutAction)
|
|
||||||
{
|
|
||||||
await _vm.VaultTimeoutActionAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.UnlockWithPIN)
|
|
||||||
{
|
|
||||||
await _vm.UpdatePinAsync();
|
|
||||||
}
|
|
||||||
else if (item.Name == AppResources.SubmitCrashLogs)
|
|
||||||
{
|
|
||||||
await _vm.LoggerReportingAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var biometricName = AppResources.Biometrics;
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
|
||||||
{
|
|
||||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
|
||||||
biometricName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
|
||||||
}
|
|
||||||
if (item.Name == string.Format(AppResources.UnlockWith, biometricName))
|
|
||||||
{
|
|
||||||
await _vm.UpdateBiometricAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -12,7 +13,9 @@ namespace Bit.App.Pages
|
|||||||
public string SubLabel { get; set; }
|
public string SubLabel { get; set; }
|
||||||
public TimeSpan? Time { get; set; }
|
public TimeSpan? Time { get; set; }
|
||||||
public bool UseFrame { get; set; }
|
public bool UseFrame { get; set; }
|
||||||
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
|
public Func<Task> ExecuteAsync { get; set; }
|
||||||
|
|
||||||
|
public bool SubLabelTextEnabled => SubLabel == AppResources.On;
|
||||||
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
|
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
|
||||||
public bool ShowSubLabel => SubLabel.Length != 0;
|
public bool ShowSubLabel => SubLabel.Length != 0;
|
||||||
public bool ShowTimeInput => Time != null;
|
public bool ShowTimeInput => Time != null;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Pages.Accounts;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -30,11 +30,13 @@ namespace Bit.App.Pages
|
|||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly ILogger _loggerService;
|
private readonly ILogger _loggerService;
|
||||||
|
|
||||||
private const int CustomVaultTimeoutValue = -100;
|
private const int CustomVaultTimeoutValue = -100;
|
||||||
|
|
||||||
private bool _supportsBiometric;
|
private bool _supportsBiometric;
|
||||||
private bool _pin;
|
private bool _pin;
|
||||||
private bool _biometric;
|
private bool _biometric;
|
||||||
|
private bool _screenCaptureAllowed;
|
||||||
private string _lastSyncDate;
|
private string _lastSyncDate;
|
||||||
private string _vaultTimeoutDisplayValue;
|
private string _vaultTimeoutDisplayValue;
|
||||||
private string _vaultTimeoutActionDisplayValue;
|
private string _vaultTimeoutActionDisplayValue;
|
||||||
@@ -84,10 +86,14 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
||||||
PageTitle = AppResources.Settings;
|
PageTitle = AppResources.Settings;
|
||||||
|
|
||||||
|
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
|
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
|
||||||
|
|
||||||
|
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||||
@@ -117,6 +123,7 @@ namespace Bit.App.Pages
|
|||||||
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||||
_pin = pinSet.Item1 || pinSet.Item2;
|
_pin = pinSet.Item1 || pinSet.Item2;
|
||||||
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||||
|
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||||
|
|
||||||
if (_vaultTimeoutDisplayValue == null)
|
if (_vaultTimeoutDisplayValue == null)
|
||||||
{
|
{
|
||||||
@@ -257,6 +264,17 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
||||||
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
|
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
|
||||||
|
|
||||||
|
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
|
||||||
|
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
|
||||||
|
{
|
||||||
|
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning,
|
||||||
|
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
_vaultTimeoutDisplayValue = selectionOption.Key;
|
_vaultTimeoutDisplayValue = selectionOption.Key;
|
||||||
newTimeout = selectionOption.Value;
|
newTimeout = selectionOption.Value;
|
||||||
}
|
}
|
||||||
@@ -423,6 +441,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public void BuildList()
|
public void BuildList()
|
||||||
{
|
{
|
||||||
|
//TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync...
|
||||||
|
|
||||||
var doUpper = Device.RuntimePlatform != Device.Android;
|
var doUpper = Device.RuntimePlatform != Device.Android;
|
||||||
var autofillItems = new List<SettingsPageListItem>();
|
var autofillItems = new List<SettingsPageListItem>();
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
@@ -430,38 +450,69 @@ namespace Bit.App.Pages
|
|||||||
autofillItems.Add(new SettingsPageListItem
|
autofillItems.Add(new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.AutofillServices,
|
Name = AppResources.AutofillServices,
|
||||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ?
|
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
|
||||||
AppResources.Enabled : AppResources.Disabled
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill });
|
autofillItems.Add(new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.PasswordAutofill,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension });
|
autofillItems.Add(new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.AppExtension,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var manageItems = new List<SettingsPageListItem>
|
var manageItems = new List<SettingsPageListItem>
|
||||||
{
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.Folders },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate }
|
{
|
||||||
|
Name = AppResources.Folders,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage()))
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.Sync,
|
||||||
|
SubLabel = _lastSyncDate,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage()))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var securityItems = new List<SettingsPageListItem>
|
var securityItems = new List<SettingsPageListItem>
|
||||||
{
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue },
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.VaultTimeout,
|
||||||
|
SubLabel = _vaultTimeoutDisplayValue,
|
||||||
|
ExecuteAsync = () => VaultTimeoutAsync() },
|
||||||
new SettingsPageListItem
|
new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.VaultTimeoutAction,
|
Name = AppResources.VaultTimeoutAction,
|
||||||
SubLabel = _vaultTimeoutActionDisplayValue
|
SubLabel = _vaultTimeoutActionDisplayValue,
|
||||||
|
ExecuteAsync = () => VaultTimeoutActionAsync()
|
||||||
},
|
},
|
||||||
new SettingsPageListItem
|
new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.UnlockWithPIN,
|
Name = AppResources.UnlockWithPIN,
|
||||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled
|
SubLabel = _pin ? AppResources.On : AppResources.Off,
|
||||||
|
ExecuteAsync = () => UpdatePinAsync()
|
||||||
},
|
},
|
||||||
new SettingsPageListItem { Name = AppResources.LockNow },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
|
{
|
||||||
|
Name = AppResources.LockNow,
|
||||||
|
ExecuteAsync = () => LockAsync()
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.TwoStepLogin,
|
||||||
|
ExecuteAsync = () => TwoStepAsync()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (_supportsBiometric || _biometric)
|
if (_supportsBiometric || _biometric)
|
||||||
{
|
{
|
||||||
@@ -474,7 +525,8 @@ namespace Bit.App.Pages
|
|||||||
var item = new SettingsPageListItem
|
var item = new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
|
SubLabel = _biometric ? AppResources.On : AppResources.Off,
|
||||||
|
ExecuteAsync = () => UpdateBiometricAsync()
|
||||||
};
|
};
|
||||||
securityItems.Insert(2, item);
|
securityItems.Insert(2, item);
|
||||||
}
|
}
|
||||||
@@ -497,40 +549,98 @@ namespace Bit.App.Pages
|
|||||||
UseFrame = true,
|
UseFrame = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
|
{
|
||||||
|
securityItems.Add(new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.AllowScreenCapture,
|
||||||
|
SubLabel = _screenCaptureAllowed ? AppResources.On : AppResources.Off,
|
||||||
|
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
|
||||||
|
});
|
||||||
|
}
|
||||||
var accountItems = new List<SettingsPageListItem>
|
var accountItems = new List<SettingsPageListItem>
|
||||||
{
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.LogOut }
|
{
|
||||||
|
Name = AppResources.FingerprintPhrase,
|
||||||
|
ExecuteAsync = () => FingerprintAsync()
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.LogOut,
|
||||||
|
ExecuteAsync = () => LogOutAsync()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (_showChangeMasterPassword)
|
if (_showChangeMasterPassword)
|
||||||
{
|
{
|
||||||
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
|
accountItems.Insert(0, new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.ChangeMasterPassword,
|
||||||
|
ExecuteAsync = () => ChangePasswordAsync()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var toolsItems = new List<SettingsPageListItem>
|
var toolsItems = new List<SettingsPageListItem>
|
||||||
{
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.ImportItems },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.ExportVault }
|
{
|
||||||
|
Name = AppResources.ImportItems,
|
||||||
|
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.ExportVault,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (IncludeLinksWithSubscriptionInfo())
|
if (IncludeLinksWithSubscriptionInfo())
|
||||||
{
|
{
|
||||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
|
toolsItems.Add(new SettingsPageListItem
|
||||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
|
{
|
||||||
|
Name = AppResources.LearnOrg,
|
||||||
|
ExecuteAsync = () => ShareAsync()
|
||||||
|
});
|
||||||
|
toolsItems.Add(new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.WebVault,
|
||||||
|
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var otherItems = new List<SettingsPageListItem>
|
var otherItems = new List<SettingsPageListItem>
|
||||||
{
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.Options },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.About },
|
{
|
||||||
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
|
Name = AppResources.Options,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage()))
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.About,
|
||||||
|
ExecuteAsync = () => AboutAsync()
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.HelpAndFeedback,
|
||||||
|
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help())
|
||||||
|
},
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
new SettingsPageListItem
|
new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.SubmitCrashLogs,
|
Name = AppResources.SubmitCrashLogs,
|
||||||
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
|
SubLabel = _reportLoggingEnabled ? AppResources.On : AppResources.Off,
|
||||||
|
ExecuteAsync = () => LoggerReportingAsync()
|
||||||
},
|
},
|
||||||
#endif
|
#endif
|
||||||
new SettingsPageListItem { Name = AppResources.RateTheApp },
|
new SettingsPageListItem
|
||||||
new SettingsPageListItem { Name = AppResources.DeleteAccount }
|
{
|
||||||
|
Name = AppResources.RateTheApp,
|
||||||
|
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate())
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.DeleteAccount,
|
||||||
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
|
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
|
||||||
@@ -610,5 +720,33 @@ namespace Bit.App.Pages
|
|||||||
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
|
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
|
||||||
|
|
||||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
|
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
|
||||||
|
|
||||||
|
public async Task SetScreenCaptureAllowedAsync()
|
||||||
|
{
|
||||||
|
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_screenCaptureAllowed
|
||||||
|
&&
|
||||||
|
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _stateService.SetScreenCaptureAllowedAsync(!_screenCaptureAllowed);
|
||||||
|
_screenCaptureAllowed = !_screenCaptureAllowed;
|
||||||
|
await _deviceActionService.SetScreenCaptureAllowedAsync();
|
||||||
|
BuildList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_loggerService.Exception(ex);
|
||||||
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Bit.App.Models;
|
|||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -55,21 +56,28 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
|
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "syncStarted")
|
try
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
if (message.Command == "syncStarted")
|
||||||
}
|
|
||||||
else if (message.Command == "syncCompleted")
|
|
||||||
{
|
|
||||||
await Task.Delay(500);
|
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||||
if (_vm.LoadedOnce)
|
}
|
||||||
|
else if (message.Command == "syncCompleted")
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
var task = _vm.LoadAsync();
|
IsBusy = false;
|
||||||
}
|
if (_vm.LoadedOnce)
|
||||||
});
|
{
|
||||||
|
var task = _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,11 +137,11 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
||||||
{
|
{
|
||||||
var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
||||||
fromAutofill: true);
|
fromAutofill: true);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
76
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public abstract class BaseCipherViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private readonly IAuditService _auditService;
|
||||||
|
protected readonly IDeviceActionService _deviceActionService;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private CipherView _cipher;
|
||||||
|
protected abstract string[] AdditionalPropertiesToRaiseOnCipherChanged { get; }
|
||||||
|
|
||||||
|
public BaseCipherViewModel()
|
||||||
|
{
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
|
CheckPasswordCommand = new AsyncCommand(CheckPasswordAsync, allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CipherView Cipher
|
||||||
|
{
|
||||||
|
get => _cipher;
|
||||||
|
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncCommand CheckPasswordCommand { get; }
|
||||||
|
|
||||||
|
protected async Task CheckPasswordAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Cipher?.Login?.Password))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
||||||
|
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
await _platformUtilsService.ShowDialogAsync(matches > 0
|
||||||
|
? string.Format(AppResources.PasswordExposed, matches.ToString("N0"))
|
||||||
|
: AppResources.PasswordSafe);
|
||||||
|
}
|
||||||
|
catch (ApiException apiException)
|
||||||
|
{
|
||||||
|
_logger.Exception(apiException);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
if (apiException?.Error != null)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(apiException.Error.GetSingleMessage(),
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<pages:BaseContentPage
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.AddEditPage"
|
x:Class="Bit.App.Pages.CipherAddEditPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
x:DataType="pages:AddEditPageViewModel"
|
x:DataType="pages:CipherAddEditPageViewModel"
|
||||||
x:Name="_page"
|
x:Name="_page"
|
||||||
Title="{Binding PageTitle}">
|
Title="{Binding PageTitle}">
|
||||||
<ContentPage.BindingContext>
|
<ContentPage.BindingContext>
|
||||||
<pages:AddEditPageViewModel />
|
<pages:CipherAddEditPageViewModel />
|
||||||
</ContentPage.BindingContext>
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
@@ -184,31 +184,61 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n AuthenticatorKey}"
|
Text="{u:I18n AuthenticatorKey}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
|
<Frame
|
||||||
|
IsVisible="{Binding HasTotpValue, Converter={StaticResource inverseBool}}"
|
||||||
|
Margin="0,5,0,0"
|
||||||
|
StyleClass="btn-icon-row"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
Padding="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="3">
|
||||||
|
<Frame.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="ScanTotp_Clicked" />
|
||||||
|
</Frame.GestureRecognizers>
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding SetupTotpText}"
|
||||||
|
Padding="0,15"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
VerticalTextAlignment="Center" />
|
||||||
|
</Frame>
|
||||||
<controls:MonoEntry
|
<controls:MonoEntry
|
||||||
x:Name="_loginTotpEntry"
|
x:Name="_loginTotpEntry"
|
||||||
Text="{Binding Cipher.Login.Totp}"
|
Text="{Binding Cipher.Login.Totp}"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
|
IsVisible="{Binding HasTotpValue}"
|
||||||
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
|
||||||
IsEnabled="{Binding Cipher.ViewPassword}"
|
IsEnabled="{Binding Cipher.ViewPassword}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
|
Command="{Binding CopyCommand}"
|
||||||
|
IsVisible="{Binding HasTotpValue}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
|
||||||
Clicked="ScanTotp_Clicked"
|
Clicked="ScanTotp_Clicked"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="2"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
IsVisible="{Binding Cipher.ViewPassword}"
|
IsVisible="{Binding HasTotpValue}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -608,7 +638,7 @@
|
|||||||
</StackLayout>
|
</StackLayout>
|
||||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||||
<controls:RepeaterView.ItemTemplate>
|
<controls:RepeaterView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="pages:AddEditPageFieldViewModel">
|
<DataTemplate x:DataType="pages:CipherAddEditPageFieldViewModel">
|
||||||
<StackLayout Spacing="0" Padding="0">
|
<StackLayout Spacing="0" Padding="0">
|
||||||
<Grid StyleClass="box-row, box-row-input">
|
<Grid StyleClass="box-row, box-row-input">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -14,7 +14,7 @@ using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
|||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class AddEditPage : BaseContentPage
|
public partial class CipherAddEditPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
@@ -22,10 +22,10 @@ namespace Bit.App.Pages
|
|||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
|
|
||||||
private AddEditPageViewModel _vm;
|
private CipherAddEditPageViewModel _vm;
|
||||||
private bool _fromAutofill;
|
private bool _fromAutofill;
|
||||||
|
|
||||||
public AddEditPage(
|
public CipherAddEditPage(
|
||||||
string cipherId = null,
|
string cipherId = null,
|
||||||
CipherType? type = null,
|
CipherType? type = null,
|
||||||
string folderId = null,
|
string folderId = null,
|
||||||
@@ -36,7 +36,7 @@ namespace Bit.App.Pages
|
|||||||
bool fromAutofill = false,
|
bool fromAutofill = false,
|
||||||
AppOptions appOptions = null,
|
AppOptions appOptions = null,
|
||||||
bool cloneMode = false,
|
bool cloneMode = false,
|
||||||
ViewPage viewPage = null)
|
CipherDetailsPage cipherDetailsPage = null)
|
||||||
{
|
{
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
|
|||||||
_fromAutofill = fromAutofill;
|
_fromAutofill = fromAutofill;
|
||||||
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
|
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as AddEditPageViewModel;
|
_vm = BindingContext as CipherAddEditPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.CipherId = cipherId;
|
_vm.CipherId = cipherId;
|
||||||
_vm.FolderId = folderId == "none" ? null : folderId;
|
_vm.FolderId = folderId == "none" ? null : folderId;
|
||||||
@@ -57,7 +57,7 @@ namespace Bit.App.Pages
|
|||||||
_vm.DefaultName = name ?? appOptions?.SaveName;
|
_vm.DefaultName = name ?? appOptions?.SaveName;
|
||||||
_vm.DefaultUri = uri ?? appOptions?.Uri;
|
_vm.DefaultUri = uri ?? appOptions?.Uri;
|
||||||
_vm.CloneMode = cloneMode;
|
_vm.CloneMode = cloneMode;
|
||||||
_vm.ViewPage = viewPage;
|
_vm.CipherDetailsPage = cipherDetailsPage;
|
||||||
_vm.Init();
|
_vm.Init();
|
||||||
SetActivityIndicator();
|
SetActivityIndicator();
|
||||||
if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android)
|
if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android)
|
||||||
@@ -145,7 +145,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool FromAutofillFramework { get; set; }
|
public bool FromAutofillFramework { get; set; }
|
||||||
public AddEditPageViewModel ViewModel => _vm;
|
public CipherAddEditPageViewModel ViewModel => _vm;
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
@@ -11,26 +10,23 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class AddEditPageViewModel : BaseViewModel
|
public class CipherAddEditPageViewModel : BaseCipherViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly ICollectionService _collectionService;
|
private readonly ICollectionService _collectionService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
|
||||||
private readonly IAuditService _auditService;
|
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly ILogger _logger;
|
private readonly IClipboardService _clipboardService;
|
||||||
|
|
||||||
private CipherView _cipher;
|
|
||||||
private bool _showNotesSeparator;
|
private bool _showNotesSeparator;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _showCardNumber;
|
private bool _showCardNumber;
|
||||||
@@ -44,7 +40,7 @@ namespace Bit.App.Pages
|
|||||||
private bool _hasCollections;
|
private bool _hasCollections;
|
||||||
private string _previousCipherId;
|
private string _previousCipherId;
|
||||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||||
private string[] _additionalCipherProperties = new string[]
|
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||||
{
|
{
|
||||||
nameof(IsLogin),
|
nameof(IsLogin),
|
||||||
nameof(IsIdentity),
|
nameof(IsIdentity),
|
||||||
@@ -53,7 +49,9 @@ namespace Bit.App.Pages
|
|||||||
nameof(ShowUris),
|
nameof(ShowUris),
|
||||||
nameof(ShowAttachments),
|
nameof(ShowAttachments),
|
||||||
nameof(ShowCollections),
|
nameof(ShowCollections),
|
||||||
|
nameof(HasTotpValue)
|
||||||
};
|
};
|
||||||
|
|
||||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||||
new List<KeyValuePair<UriMatchType?, string>>
|
new List<KeyValuePair<UriMatchType?, string>>
|
||||||
{
|
{
|
||||||
@@ -66,31 +64,28 @@ namespace Bit.App.Pages
|
|||||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
|
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
|
||||||
};
|
};
|
||||||
|
|
||||||
public AddEditPageViewModel()
|
public CipherAddEditPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
|
|
||||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
|
||||||
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
||||||
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
|
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
|
||||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||||
|
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||||
AllowPersonal = true;
|
AllowPersonal = true;
|
||||||
|
|
||||||
@@ -146,10 +141,10 @@ namespace Bit.App.Pages
|
|||||||
public Command TogglePasswordCommand { get; set; }
|
public Command TogglePasswordCommand { get; set; }
|
||||||
public Command ToggleCardNumberCommand { get; set; }
|
public Command ToggleCardNumberCommand { get; set; }
|
||||||
public Command ToggleCardCodeCommand { get; set; }
|
public Command ToggleCardCodeCommand { get; set; }
|
||||||
public Command CheckPasswordCommand { get; set; }
|
|
||||||
public Command UriOptionsCommand { get; set; }
|
public Command UriOptionsCommand { get; set; }
|
||||||
public Command FieldOptionsCommand { get; set; }
|
public Command FieldOptionsCommand { get; set; }
|
||||||
public Command PasswordPromptHelpCommand { get; set; }
|
public Command PasswordPromptHelpCommand { get; set; }
|
||||||
|
public AsyncCommand CopyCommand { get; set; }
|
||||||
public string CipherId { get; set; }
|
public string CipherId { get; set; }
|
||||||
public string OrganizationId { get; set; }
|
public string OrganizationId { get; set; }
|
||||||
public string FolderId { get; set; }
|
public string FolderId { get; set; }
|
||||||
@@ -164,7 +159,7 @@ namespace Bit.App.Pages
|
|||||||
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
|
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
|
||||||
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
|
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
|
||||||
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
|
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
|
||||||
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
|
public ExtendedObservableCollection<CipherAddEditPageFieldViewModel> Fields { get; set; }
|
||||||
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
||||||
|
|
||||||
public int TypeSelectedIndex
|
public int TypeSelectedIndex
|
||||||
@@ -233,11 +228,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public CipherView Cipher
|
|
||||||
{
|
|
||||||
get => _cipher;
|
|
||||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
|
|
||||||
}
|
|
||||||
public bool ShowNotesSeparator
|
public bool ShowNotesSeparator
|
||||||
{
|
{
|
||||||
get => _showNotesSeparator;
|
get => _showNotesSeparator;
|
||||||
@@ -285,7 +275,7 @@ namespace Bit.App.Pages
|
|||||||
public bool ShowOwnershipOptions => !EditMode || CloneMode;
|
public bool ShowOwnershipOptions => !EditMode || CloneMode;
|
||||||
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
|
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
|
||||||
public bool CloneMode { get; set; }
|
public bool CloneMode { get; set; }
|
||||||
public ViewPage ViewPage { get; set; }
|
public CipherDetailsPage CipherDetailsPage { get; set; }
|
||||||
public bool IsLogin => Cipher?.Type == CipherType.Login;
|
public bool IsLogin => Cipher?.Type == CipherType.Login;
|
||||||
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
|
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
|
||||||
public bool IsCard => Cipher?.Type == CipherType.Card;
|
public bool IsCard => Cipher?.Type == CipherType.Card;
|
||||||
@@ -300,7 +290,8 @@ namespace Bit.App.Pages
|
|||||||
public bool AllowPersonal { get; set; }
|
public bool AllowPersonal { get; set; }
|
||||||
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
|
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
|
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
|
||||||
|
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
||||||
@@ -421,7 +412,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (Cipher.Fields != null)
|
if (Cipher.Fields != null)
|
||||||
{
|
{
|
||||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
|
Fields.ResetWithRange(Cipher.Fields?.Select(f => new CipherAddEditPageFieldViewModel(Cipher, f)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,7 +500,7 @@ namespace Bit.App.Pages
|
|||||||
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
|
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
|
||||||
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
|
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
|
||||||
|
|
||||||
if (Page is AddEditPage page && page.FromAutofillFramework)
|
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
|
||||||
{
|
{
|
||||||
// Close and go back to app
|
// Close and go back to app
|
||||||
_deviceActionService.CloseAutofill();
|
_deviceActionService.CloseAutofill();
|
||||||
@@ -518,7 +509,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (CloneMode)
|
if (CloneMode)
|
||||||
{
|
{
|
||||||
ViewPage?.UpdateCipherId(this.Cipher.Id);
|
CipherDetailsPage?.UpdateCipherId(this.Cipher.Id);
|
||||||
}
|
}
|
||||||
// if the app is tombstoned then PopModalAsync would throw index out of bounds
|
// if the app is tombstoned then PopModalAsync would throw index out of bounds
|
||||||
if (Page.Navigation?.ModalStack?.Count > 0)
|
if (Page.Navigation?.ModalStack?.Count > 0)
|
||||||
@@ -603,7 +594,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async void UriOptions(LoginUriView uri)
|
public async void UriOptions(LoginUriView uri)
|
||||||
{
|
{
|
||||||
if (!(Page as AddEditPage).DoOnce())
|
if (!(Page as CipherAddEditPage).DoOnce())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -639,9 +630,9 @@ namespace Bit.App.Pages
|
|||||||
Uris.Add(new LoginUriView());
|
Uris.Add(new LoginUriView());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void FieldOptions(AddEditPageFieldViewModel field)
|
public async void FieldOptions(CipherAddEditPageFieldViewModel field)
|
||||||
{
|
{
|
||||||
if (!(Page as AddEditPage).DoOnce())
|
if (!(Page as CipherAddEditPage).DoOnce())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -701,10 +692,10 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (Fields == null)
|
if (Fields == null)
|
||||||
{
|
{
|
||||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||||
}
|
}
|
||||||
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
|
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
|
||||||
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
|
Fields.Add(new CipherAddEditPageFieldViewModel(Cipher, new FieldView
|
||||||
{
|
{
|
||||||
Type = type,
|
Type = type,
|
||||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||||
@@ -832,35 +823,24 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void TriggerCipherChanged()
|
private void TriggerCipherChanged()
|
||||||
{
|
{
|
||||||
TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties);
|
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CheckPasswordAsync()
|
private async Task CopyTotpClipboardAsync()
|
||||||
{
|
{
|
||||||
if (!(Page as BaseContentPage).DoOnce())
|
try
|
||||||
{
|
{
|
||||||
return;
|
await _clipboardService.CopyTextAsync(Cipher.Login.Totp);
|
||||||
|
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
|
||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return;
|
_logger.Exception(ex);
|
||||||
}
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
|
||||||
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
if (matches > 0)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
|
|
||||||
matches.ToString("N0")));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AddEditPageFieldViewModel : ExtendedViewModel
|
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
|
||||||
{
|
{
|
||||||
private II18nService _i18nService;
|
private II18nService _i18nService;
|
||||||
private FieldView _field;
|
private FieldView _field;
|
||||||
@@ -876,7 +856,7 @@ namespace Bit.App.Pages
|
|||||||
nameof(IsLinkedType),
|
nameof(IsLinkedType),
|
||||||
};
|
};
|
||||||
|
|
||||||
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
|
public CipherAddEditPageFieldViewModel(CipherView cipher, FieldView field)
|
||||||
{
|
{
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_cipher = cipher;
|
_cipher = cipher;
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<pages:BaseContentPage
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.ViewPage"
|
x:Class="Bit.App.Pages.CipherDetailsPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
|
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
x:DataType="pages:ViewPageViewModel"
|
x:DataType="pages:CipherDetailsPageViewModel"
|
||||||
x:Name="_page"
|
x:Name="_page"
|
||||||
Title="{Binding PageTitle}">
|
Title="{Binding PageTitle}">
|
||||||
<ContentPage.BindingContext>
|
<ContentPage.BindingContext>
|
||||||
<pages:ViewPageViewModel />
|
<pages:CipherDetailsPageViewModel />
|
||||||
</ContentPage.BindingContext>
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
@@ -22,15 +22,15 @@
|
|||||||
<u:StringHasValueConverter x:Key="stringHasValue" />
|
<u:StringHasValueConverter x:Key="stringHasValue" />
|
||||||
<u:IsNotNullConverter x:Key="notNull" />
|
<u:IsNotNullConverter x:Key="notNull" />
|
||||||
<ToolbarItem Text="{u:I18n Collections}"
|
<ToolbarItem Text="{u:I18n Collections}"
|
||||||
x:Key="collectionsItem"
|
x:Key="collectionsItem"
|
||||||
x:Name="_collectionsItem"
|
x:Name="_collectionsItem"
|
||||||
Clicked="Collections_Clicked"
|
Clicked="Collections_Clicked"
|
||||||
Order="Secondary" />
|
Order="Secondary" />
|
||||||
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
||||||
x:Key="shareItem"
|
x:Key="shareItem"
|
||||||
x:Name="_shareItem"
|
x:Name="_shareItem"
|
||||||
Clicked="Share_Clicked"
|
Clicked="Share_Clicked"
|
||||||
Order="Secondary" />
|
Order="Secondary" />
|
||||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
x:Name="_closeItem" x:Key="closeItem" />
|
x:Name="_closeItem" x:Key="closeItem" />
|
||||||
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
|
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
<BoxView StyleClass="box-row-separator"
|
<BoxView StyleClass="box-row-separator"
|
||||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
||||||
<Grid StyleClass="box-row"
|
<Grid StyleClass="box-row"
|
||||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
|
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
LineBreakMode="CharacterWrap"
|
LineBreakMode="CharacterWrap"
|
||||||
IsVisible="{Binding ShowPassword}" />
|
IsVisible="{Binding ShowPassword}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||||
Command="{Binding CheckPasswordCommand}"
|
Command="{Binding CheckPasswordCommand}"
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
Command="{Binding TogglePasswordCommand}"
|
Command="{Binding TogglePasswordCommand}"
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
@@ -165,10 +165,11 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="40" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label
|
<Label
|
||||||
@@ -178,29 +179,49 @@
|
|||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:MonoLabel
|
<controls:MonoLabel
|
||||||
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
|
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
|
||||||
|
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
<Label
|
VerticalTextAlignment="Start"
|
||||||
Text="{Binding TotpSec, Mode=OneWay}"
|
VerticalOptions="Start" />
|
||||||
Style="{DynamicResource textTotp}"
|
<controls:CircularProgressbarView
|
||||||
Margin="0, 0, 10, 0"
|
Progress="{Binding TotpProgress}"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
HorizontalOptions="End"
|
HorizontalOptions="FillAndExpand"
|
||||||
HorizontalTextAlignment="End"
|
VerticalOptions="FillAndExpand" />
|
||||||
VerticalOptions="CenterAndExpand" />
|
<Label
|
||||||
<controls:IconButton
|
Text="{Binding TotpSec, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
|
IsVisible="{Binding CanAccessPremium}"
|
||||||
CommandParameter="LoginTotp"
|
CommandParameter="LoginTotp"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n PremiumSubscriptionRequired}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
IsVisible="{Binding ShowUpgradePremiumTotpText}"
|
||||||
|
Margin="0,5,0,2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalOptions="FillAndExpand" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
@@ -244,7 +265,7 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding ShowCardNumber}" />
|
IsVisible="{Binding ShowCardNumber}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowCardNumberIcon}"
|
Text="{Binding ShowCardNumberIcon}"
|
||||||
Command="{Binding ToggleCardNumberCommand}"
|
Command="{Binding ToggleCardNumberCommand}"
|
||||||
@@ -253,7 +274,7 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
@@ -316,7 +337,7 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding ShowCardCode}" />
|
IsVisible="{Binding ShowCardCode}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowCardCodeIcon}"
|
Text="{Binding ShowCardCodeIcon}"
|
||||||
Command="{Binding ToggleCardCodeCommand}"
|
Command="{Binding ToggleCardCodeCommand}"
|
||||||
@@ -325,7 +346,7 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
@@ -529,7 +550,7 @@
|
|||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<controls:SelectableLabel
|
<controls:SelectableLabel
|
||||||
Text="{Binding Cipher.Notes, Mode=OneWay}"
|
Text="{Binding Cipher.Notes, Mode=OneWay}"
|
||||||
StyleClass="box-value"/>
|
StyleClass="box-value" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
@@ -540,7 +561,7 @@
|
|||||||
</StackLayout>
|
</StackLayout>
|
||||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||||
<controls:RepeaterView.ItemTemplate>
|
<controls:RepeaterView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="pages:ViewPageFieldViewModel">
|
<DataTemplate x:DataType="pages:CipherDetailsPageFieldViewModel">
|
||||||
<StackLayout Spacing="0" Padding="0">
|
<StackLayout Spacing="0" Padding="0">
|
||||||
<Grid StyleClass="box-row">
|
<Grid StyleClass="box-row">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -571,6 +592,8 @@
|
|||||||
IsVisible="{Binding IsLinkedType}" />
|
IsVisible="{Binding IsLinkedType}" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="true"
|
||||||
|
AutomationProperties.Name="{Binding ValueAccessibilityText, Mode=OneWay}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -588,7 +611,7 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowHiddenValueIcon}"
|
Text="{Binding ShowHiddenValueIcon}"
|
||||||
Command="{Binding ToggleHiddenValueCommand}"
|
Command="{Binding ToggleHiddenValueCommand}"
|
||||||
@@ -636,7 +659,7 @@
|
|||||||
StyleClass="box-sub-label"
|
StyleClass="box-sub-label"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="End"
|
||||||
VerticalTextAlignment="Center" />
|
VerticalTextAlignment="Center" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
|
||||||
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
|
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
|
||||||
@@ -700,4 +723,4 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</AbsoluteLayout>
|
</AbsoluteLayout>
|
||||||
|
|
||||||
</pages:BaseContentPage>
|
</pages:BaseContentPage>
|
||||||
@@ -3,23 +3,24 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class ViewPage : BaseContentPage
|
public partial class CipherDetailsPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
private ViewPageViewModel _vm;
|
private CipherDetailsPageViewModel _vm;
|
||||||
|
|
||||||
public ViewPage(string cipherId)
|
public CipherDetailsPage(string cipherId)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_vm = BindingContext as ViewPageViewModel;
|
_vm = BindingContext as CipherDetailsPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.CipherId = cipherId;
|
_vm.CipherId = cipherId;
|
||||||
SetActivityIndicator(_mainContent);
|
SetActivityIndicator(_mainContent);
|
||||||
@@ -39,7 +40,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewPageViewModel ViewModel => _vm;
|
public CipherDetailsPageViewModel ViewModel => _vm;
|
||||||
|
|
||||||
public void UpdateCipherId(string cipherId)
|
public void UpdateCipherId(string cipherId)
|
||||||
{
|
{
|
||||||
@@ -54,39 +55,46 @@ namespace Bit.App.Pages
|
|||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
|
_broadcasterService.Subscribe(nameof(CipherDetailsPage), async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "syncStarted")
|
try
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
if (message.Command == "syncStarted")
|
||||||
}
|
|
||||||
else if (message.Command == "syncCompleted")
|
|
||||||
{
|
|
||||||
await Task.Delay(500);
|
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||||
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
|
}
|
||||||
|
else if (message.Command == "syncCompleted")
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
var success = data["successfully"] as bool?;
|
IsBusy = false;
|
||||||
if (success.GetValueOrDefault())
|
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
|
||||||
{
|
{
|
||||||
var task = _vm.LoadAsync(() => AdjustToolbar());
|
var success = data["successfully"] as bool?;
|
||||||
|
if (success.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
var task = _vm.LoadAsync(() => AdjustToolbar());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
else if (message.Command == "selectSaveFileResult")
|
||||||
else if (message.Command == "selectSaveFileResult")
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
{
|
||||||
var data = message.Data as Tuple<string, string>;
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
if (data == null)
|
|
||||||
{
|
{
|
||||||
return;
|
var data = message.Data as Tuple<string, string>;
|
||||||
}
|
if (data == null)
|
||||||
_vm.SaveFileSelected(data.Item1, data.Item2);
|
{
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
_vm.SaveFileSelected(data.Item1, data.Item2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||||
@@ -103,8 +111,8 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
base.OnDisappearing();
|
base.OnDisappearing();
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
_broadcasterService.Unsubscribe(nameof(ViewPage));
|
_vm.StopCiphersTotpTick().FireAndForget();
|
||||||
_vm.CleanUp();
|
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
|
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
|
||||||
@@ -132,7 +140,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_vm.CipherId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +212,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +267,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
else if (selection == AppResources.Clone)
|
else if (selection == AppResources.Clone)
|
||||||
{
|
{
|
||||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -11,26 +13,24 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class ViewPageViewModel : BaseViewModel
|
public class CipherDetailsPageViewModel : BaseCipherViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly ITotpService _totpService;
|
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
|
||||||
private readonly IAuditService _auditService;
|
private readonly IAuditService _auditService;
|
||||||
|
private readonly ITotpService _totpService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
private readonly ILocalizeService _localizeService;
|
private readonly ILocalizeService _localizeService;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
|
|
||||||
private CipherView _cipher;
|
private List<CipherDetailsPageFieldViewModel> _fields;
|
||||||
private List<ViewPageFieldViewModel> _fields;
|
|
||||||
private bool _canAccessPremium;
|
private bool _canAccessPremium;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _showCardNumber;
|
private bool _showCardNumber;
|
||||||
@@ -44,67 +44,62 @@ namespace Bit.App.Pages
|
|||||||
private byte[] _attachmentData;
|
private byte[] _attachmentData;
|
||||||
private string _attachmentFilename;
|
private string _attachmentFilename;
|
||||||
private bool _passwordReprompted;
|
private bool _passwordReprompted;
|
||||||
|
private TotpHelper _totpTickHelper;
|
||||||
|
private CancellationTokenSource _totpTickCancellationToken;
|
||||||
|
private Task _totpTickTask;
|
||||||
|
|
||||||
public ViewPageViewModel()
|
public CipherDetailsPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||||
|
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
|
|
||||||
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
|
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||||
CopyUriCommand = new Command<LoginUriView>(CopyUri);
|
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||||
CopyFieldCommand = new Command<FieldView>(CopyField);
|
CopyFieldCommand = new AsyncCommand<FieldView>(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||||
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
DownloadAttachmentCommand = new AsyncCommand<AttachmentView>(DownloadAttachmentAsync, allowsMultipleExecutions: false);
|
||||||
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
|
|
||||||
|
|
||||||
PageTitle = AppResources.ViewItem;
|
PageTitle = AppResources.ViewItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command CopyCommand { get; set; }
|
public ICommand CopyCommand { get; set; }
|
||||||
public Command CopyUriCommand { get; set; }
|
public ICommand CopyUriCommand { get; set; }
|
||||||
public Command CopyFieldCommand { get; set; }
|
public ICommand CopyFieldCommand { get; set; }
|
||||||
public Command LaunchUriCommand { get; set; }
|
public Command LaunchUriCommand { get; set; }
|
||||||
public Command TogglePasswordCommand { get; set; }
|
public Command TogglePasswordCommand { get; set; }
|
||||||
public Command ToggleCardNumberCommand { get; set; }
|
public Command ToggleCardNumberCommand { get; set; }
|
||||||
public Command ToggleCardCodeCommand { get; set; }
|
public Command ToggleCardCodeCommand { get; set; }
|
||||||
public Command CheckPasswordCommand { get; set; }
|
public AsyncCommand<AttachmentView> DownloadAttachmentCommand { get; set; }
|
||||||
public Command DownloadAttachmentCommand { get; set; }
|
|
||||||
public string CipherId { get; set; }
|
public string CipherId { get; set; }
|
||||||
public CipherView Cipher
|
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||||
{
|
{
|
||||||
get => _cipher;
|
nameof(IsLogin),
|
||||||
set => SetProperty(ref _cipher, value,
|
nameof(IsIdentity),
|
||||||
additionalPropertyNames: new string[]
|
nameof(IsCard),
|
||||||
{
|
nameof(IsSecureNote),
|
||||||
nameof(IsLogin),
|
nameof(ShowUris),
|
||||||
nameof(IsIdentity),
|
nameof(ShowAttachments),
|
||||||
nameof(IsCard),
|
nameof(ShowTotp),
|
||||||
nameof(IsSecureNote),
|
nameof(ColoredPassword),
|
||||||
nameof(ShowUris),
|
nameof(UpdatedText),
|
||||||
nameof(ShowAttachments),
|
nameof(PasswordUpdatedText),
|
||||||
nameof(ShowTotp),
|
nameof(PasswordHistoryText),
|
||||||
nameof(ColoredPassword),
|
nameof(ShowIdentityAddress),
|
||||||
nameof(UpdatedText),
|
nameof(IsDeleted),
|
||||||
nameof(PasswordUpdatedText),
|
nameof(CanEdit),
|
||||||
nameof(PasswordHistoryText),
|
nameof(ShowUpgradePremiumTotpText)
|
||||||
nameof(ShowIdentityAddress),
|
};
|
||||||
nameof(IsDeleted),
|
public List<CipherDetailsPageFieldViewModel> Fields
|
||||||
nameof(CanEdit),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
public List<ViewPageFieldViewModel> Fields
|
|
||||||
{
|
{
|
||||||
get => _fields;
|
get => _fields;
|
||||||
set => SetProperty(ref _fields, value);
|
set => SetProperty(ref _fields, value);
|
||||||
@@ -203,21 +198,22 @@ namespace Bit.App.Pages
|
|||||||
return fs;
|
return fs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && !Cipher.OrganizationUseTotp && ShowTotp;
|
||||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||||
public bool ShowIdentityAddress => IsIdentity && (
|
public bool ShowIdentityAddress => IsIdentity && (
|
||||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
||||||
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
|
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
|
||||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
|
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
|
||||||
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
|
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
|
||||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
|
||||||
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
|
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string TotpCodeFormatted
|
public string TotpCodeFormatted
|
||||||
{
|
{
|
||||||
get => _totpCodeFormatted;
|
get => ShowUpgradePremiumTotpText ? string.Empty : _totpCodeFormatted;
|
||||||
set => SetProperty(ref _totpCodeFormatted, value,
|
set => SetProperty(ref _totpCodeFormatted, value,
|
||||||
additionalPropertyNames: new string[]
|
additionalPropertyNames: new string[]
|
||||||
{
|
{
|
||||||
@@ -227,7 +223,11 @@ namespace Bit.App.Pages
|
|||||||
public string TotpSec
|
public string TotpSec
|
||||||
{
|
{
|
||||||
get => _totpSec;
|
get => _totpSec;
|
||||||
set => SetProperty(ref _totpSec, value);
|
set => SetProperty(ref _totpSec, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(TotpProgress)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public bool TotpLow
|
public bool TotpLow
|
||||||
{
|
{
|
||||||
@@ -238,12 +238,12 @@ namespace Bit.App.Pages
|
|||||||
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
|
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / 30;
|
||||||
public bool IsDeleted => Cipher.IsDeleted;
|
public bool IsDeleted => Cipher.IsDeleted;
|
||||||
public bool CanEdit => !Cipher.IsDeleted;
|
public bool CanEdit => !Cipher.IsDeleted;
|
||||||
|
|
||||||
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||||
{
|
{
|
||||||
CleanUp();
|
|
||||||
var cipher = await _cipherService.GetAsync(CipherId);
|
var cipher = await _cipherService.GetAsync(CipherId);
|
||||||
if (cipher == null)
|
if (cipher == null)
|
||||||
{
|
{
|
||||||
@@ -252,24 +252,15 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
Cipher = await cipher.DecryptAsync();
|
Cipher = await cipher.DecryptAsync();
|
||||||
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
|
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
|
Fields = Cipher.Fields?.Select(f => new CipherDetailsPageFieldViewModel(this, Cipher, f)).ToList();
|
||||||
|
|
||||||
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||||
{
|
{
|
||||||
await TotpUpdateCodeAsync();
|
_totpTickHelper = new TotpHelper(Cipher);
|
||||||
var interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
|
_totpTickCancellationToken?.Cancel();
|
||||||
await TotpTickAsync(interval);
|
_totpTickCancellationToken = new CancellationTokenSource();
|
||||||
_totpInterval = DateTime.UtcNow;
|
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
|
||||||
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
|
|
||||||
{
|
|
||||||
if (_totpInterval == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var task = TotpTickAsync(interval);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (_previousCipherId != CipherId)
|
if (_previousCipherId != CipherId)
|
||||||
{
|
{
|
||||||
@@ -280,9 +271,27 @@ namespace Bit.App.Pages
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CleanUp()
|
private async void StartCiphersTotpTick()
|
||||||
{
|
{
|
||||||
_totpInterval = null;
|
try
|
||||||
|
{
|
||||||
|
await _totpTickHelper.GenerateNewTotpValues();
|
||||||
|
TotpSec = _totpTickHelper.TotpSec;
|
||||||
|
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopCiphersTotpTick()
|
||||||
|
{
|
||||||
|
_totpTickCancellationToken?.Cancel();
|
||||||
|
if (_totpTickTask != null)
|
||||||
|
{
|
||||||
|
await _totpTickTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void TogglePassword()
|
public async void TogglePassword()
|
||||||
@@ -451,86 +460,52 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CheckPasswordAsync()
|
private async Task DownloadAttachmentAsync(AttachmentView attachment)
|
||||||
{
|
{
|
||||||
if (!(Page as BaseContentPage).DoOnce())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
||||||
AppResources.InternetConnectionRequiredTitle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
|
|
||||||
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
if (matches > 0)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
|
|
||||||
matches.ToString("N0")));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void DownloadAttachmentAsync(AttachmentView attachment)
|
|
||||||
{
|
|
||||||
if (!(Page as BaseContentPage).DoOnce())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
||||||
AppResources.InternetConnectionRequiredTitle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (attachment.FileSize >= 10485760) // 10 MB
|
|
||||||
{
|
|
||||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
|
||||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
|
||||||
AppResources.Yes, AppResources.No);
|
|
||||||
if (!confirmed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canOpenFile = true;
|
|
||||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
|
||||||
{
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
|
||||||
{
|
|
||||||
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
|
||||||
// for any reason we want to be sure to catch it here.
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canOpenFile = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await PromptPasswordAsync())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachment.FileSize >= 10485760) // 10 MB
|
||||||
|
{
|
||||||
|
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||||
|
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||||
|
AppResources.Yes, AppResources.No);
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canOpenFile = true;
|
||||||
|
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||||
|
{
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
||||||
|
// for any reason we want to be sure to catch it here.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canOpenFile = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await PromptPasswordAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
if (data == null)
|
if (data == null)
|
||||||
@@ -557,9 +532,11 @@ namespace Bit.App.Pages
|
|||||||
OpenAttachment(data, attachment);
|
OpenAttachment(data, attachment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +593,7 @@ namespace Bit.App.Pages
|
|||||||
_attachmentFilename = null;
|
_attachmentFilename = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CopyAsync(string id, string text = null)
|
private async Task CopyAsync(string id, string text = null)
|
||||||
{
|
{
|
||||||
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
|
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
|
||||||
{
|
{
|
||||||
@@ -636,7 +613,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
else if (id == "LoginTotp")
|
else if (id == "LoginTotp")
|
||||||
{
|
{
|
||||||
text = _totpCode;
|
text = TotpCodeFormatted.Replace(" ", string.Empty);
|
||||||
name = AppResources.VerificationCodeTotp;
|
name = AppResources.VerificationCodeTotp;
|
||||||
}
|
}
|
||||||
else if (id == "LoginUri")
|
else if (id == "LoginUri")
|
||||||
@@ -680,16 +657,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CopyUri(LoginUriView uri)
|
|
||||||
{
|
|
||||||
CopyAsync("LoginUri", uri.Uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyField(FieldView field)
|
|
||||||
{
|
|
||||||
CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LaunchUri(LoginUriView uri)
|
private void LaunchUri(LoginUriView uri)
|
||||||
{
|
{
|
||||||
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
|
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
|
||||||
@@ -709,15 +676,15 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ViewPageFieldViewModel : ExtendedViewModel
|
public class CipherDetailsPageFieldViewModel : ExtendedViewModel
|
||||||
{
|
{
|
||||||
private II18nService _i18nService;
|
private II18nService _i18nService;
|
||||||
private ViewPageViewModel _vm;
|
private CipherDetailsPageViewModel _vm;
|
||||||
private FieldView _field;
|
private FieldView _field;
|
||||||
private CipherView _cipher;
|
private CipherView _cipher;
|
||||||
private bool _showHiddenValue;
|
private bool _showHiddenValue;
|
||||||
|
|
||||||
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
|
public CipherDetailsPageFieldViewModel(CipherDetailsPageViewModel vm, CipherView cipher, FieldView field)
|
||||||
{
|
{
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_vm = vm;
|
_vm = vm;
|
||||||
@@ -733,6 +700,7 @@ namespace Bit.App.Pages
|
|||||||
additionalPropertyNames: new string[]
|
additionalPropertyNames: new string[]
|
||||||
{
|
{
|
||||||
nameof(ValueText),
|
nameof(ValueText),
|
||||||
|
nameof(ValueAccessibilityText),
|
||||||
nameof(IsBooleanType),
|
nameof(IsBooleanType),
|
||||||
nameof(IsHiddenType),
|
nameof(IsHiddenType),
|
||||||
nameof(IsTextType),
|
nameof(IsTextType),
|
||||||
@@ -756,7 +724,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (IsBooleanType)
|
if (IsBooleanType)
|
||||||
{
|
{
|
||||||
return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare;
|
return _field.BoolValue ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
|
||||||
}
|
}
|
||||||
else if (IsLinkedType)
|
else if (IsLinkedType)
|
||||||
{
|
{
|
||||||
@@ -770,6 +738,19 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ValueAccessibilityText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsBooleanType)
|
||||||
|
{
|
||||||
|
return _field.BoolValue ? AppResources.Enabled : AppResources.Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
|
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
|
||||||
|
|
||||||
public Command ToggleHiddenValueCommand { get; set; }
|
public Command ToggleHiddenValueCommand { get; set; }
|
||||||
@@ -3,22 +3,18 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class CiphersPageViewModel : BaseViewModel
|
public class CiphersPageViewModel : VaultFilterViewModel
|
||||||
{
|
{
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
@@ -31,12 +27,9 @@ namespace Bit.App.Pages
|
|||||||
private CancellationTokenSource _searchCancellationTokenSource;
|
private CancellationTokenSource _searchCancellationTokenSource;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private bool _showVaultFilter;
|
|
||||||
private string _vaultFilterSelection;
|
|
||||||
private bool _showNoData;
|
private bool _showNoData;
|
||||||
private bool _showList;
|
private bool _showList;
|
||||||
private bool _websiteIconsEnabled;
|
private bool _websiteIconsEnabled;
|
||||||
private List<Organization> _organizations;
|
|
||||||
|
|
||||||
public CiphersPageViewModel()
|
public CiphersPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -52,18 +45,19 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
Ciphers = new ExtendedObservableCollection<CipherView>();
|
Ciphers = new ExtendedObservableCollection<CipherView>();
|
||||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||||
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
|
|
||||||
onException: ex => _logger.Exception(ex),
|
|
||||||
allowsMultipleExecutions: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command CipherOptionsCommand { get; set; }
|
public Command CipherOptionsCommand { get; set; }
|
||||||
public ICommand VaultFilterCommand { get; }
|
|
||||||
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
||||||
public Func<CipherView, bool> Filter { get; set; }
|
public Func<CipherView, bool> Filter { get; set; }
|
||||||
public string AutofillUrl { get; set; }
|
public string AutofillUrl { get; set; }
|
||||||
public bool Deleted { get; set; }
|
public bool Deleted { get; set; }
|
||||||
|
|
||||||
|
protected override ICipherService cipherService => _cipherService;
|
||||||
|
protected override IPolicyService policyService => _policyService;
|
||||||
|
protected override IOrganizationService organizationService => _organizationService;
|
||||||
|
protected override ILogger logger => _logger;
|
||||||
|
|
||||||
public bool ShowNoData
|
public bool ShowNoData
|
||||||
{
|
{
|
||||||
get => _showNoData;
|
get => _showNoData;
|
||||||
@@ -81,23 +75,6 @@ namespace Bit.App.Pages
|
|||||||
nameof(ShowSearchDirection)
|
nameof(ShowSearchDirection)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public bool ShowVaultFilter
|
|
||||||
{
|
|
||||||
get => _showVaultFilter;
|
|
||||||
set => SetProperty(ref _showVaultFilter, value);
|
|
||||||
}
|
|
||||||
public string VaultFilterDescription
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
|
|
||||||
{
|
|
||||||
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
|
|
||||||
}
|
|
||||||
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
|
|
||||||
}
|
|
||||||
set => SetProperty(ref _vaultFilterSelection, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
||||||
|
|
||||||
@@ -109,12 +86,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
_organizations = await _organizationService.GetAllAsync();
|
await InitVaultFilterAsync(true);
|
||||||
ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync();
|
|
||||||
if (ShowVaultFilter && _vaultFilterSelection == null)
|
|
||||||
{
|
|
||||||
_vaultFilterSelection = AppResources.AllVaults;
|
|
||||||
}
|
|
||||||
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||||
PerformSearchIfPopulated();
|
PerformSearchIfPopulated();
|
||||||
}
|
}
|
||||||
@@ -184,7 +156,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
||||||
{
|
{
|
||||||
var page = new ViewPage(cipher.Id);
|
var page = new CipherDetailsPage(cipher.Id);
|
||||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
||||||
@@ -237,50 +209,11 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task VaultFilterOptionsAsync()
|
protected override async Task OnVaultFilterSelectedAsync()
|
||||||
{
|
{
|
||||||
var options = new List<string> { AppResources.AllVaults, AppResources.MyVault };
|
|
||||||
if (_organizations.Any())
|
|
||||||
{
|
|
||||||
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
|
|
||||||
}
|
|
||||||
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
|
|
||||||
options.ToArray());
|
|
||||||
if (selection == null || selection == AppResources.Cancel ||
|
|
||||||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
|
|
||||||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
VaultFilterDescription = selection;
|
|
||||||
PerformSearchIfPopulated();
|
PerformSearchIfPopulated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<CipherView>> GetAllCiphersAsync()
|
|
||||||
{
|
|
||||||
var decCiphers = await _cipherService.GetAllDecryptedAsync();
|
|
||||||
if (IsVaultFilterMyVault)
|
|
||||||
{
|
|
||||||
return decCiphers.Where(c => c.OrganizationId == null).ToList();
|
|
||||||
}
|
|
||||||
if (IsVaultFilterOrgVault)
|
|
||||||
{
|
|
||||||
var orgId = GetVaultFilterOrgId();
|
|
||||||
return decCiphers.Where(c => c.OrganizationId == orgId).ToList();
|
|
||||||
}
|
|
||||||
return decCiphers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
|
|
||||||
|
|
||||||
private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
|
|
||||||
_vaultFilterSelection != AppResources.MyVault;
|
|
||||||
|
|
||||||
private string GetVaultFilterOrgId()
|
|
||||||
{
|
|
||||||
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void CipherOptionsAsync(CipherView cipher)
|
private async void CipherOptionsAsync(CipherView cipher)
|
||||||
{
|
{
|
||||||
if ((Page as BaseContentPage).DoOnce())
|
if ((Page as BaseContentPage).DoOnce())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
x:DataType="pages:GroupingsPageViewModel"
|
x:DataType="pages:GroupingsPageViewModel"
|
||||||
Title="{Binding PageTitle}"
|
Title="{Binding PageTitle}"
|
||||||
@@ -53,6 +54,14 @@
|
|||||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
|
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="authenticatorTemplate"
|
||||||
|
x:DataType="pages:GroupingsPageTOTPListItem">
|
||||||
|
<controls:AuthenticatorViewCell
|
||||||
|
Cipher="{Binding Cipher}"
|
||||||
|
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
|
||||||
|
TotpSec="{Binding TotpSec}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate x:Key="groupTemplate"
|
<DataTemplate x:Key="groupTemplate"
|
||||||
x:DataType="pages:GroupingsPageListItem">
|
x:DataType="pages:GroupingsPageListItem">
|
||||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||||
@@ -104,6 +113,7 @@
|
|||||||
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
||||||
HeaderTemplate="{StaticResource headerTemplate}"
|
HeaderTemplate="{StaticResource headerTemplate}"
|
||||||
CipherTemplate="{StaticResource cipherTemplate}"
|
CipherTemplate="{StaticResource cipherTemplate}"
|
||||||
|
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
|
||||||
GroupTemplate="{StaticResource groupTemplate}" />
|
GroupTemplate="{StaticResource groupTemplate}" />
|
||||||
|
|
||||||
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||||
@@ -130,7 +140,6 @@
|
|||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Filter}" />
|
AutomationProperties.Name="{u:I18n Filter}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|
||||||
<StackLayout
|
<StackLayout
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
Padding="20, 0"
|
Padding="20, 0"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.App.Resources;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly GroupingsPageViewModel _vm;
|
private readonly GroupingsPageViewModel _vm;
|
||||||
private readonly string _pageName;
|
private readonly string _pageName;
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
||||||
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
|
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
|
||||||
PreviousPageInfo previousPage = null, bool deleted = false)
|
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)
|
||||||
{
|
{
|
||||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -40,6 +42,7 @@ namespace Bit.App.Pages
|
|||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_vm = BindingContext as GroupingsPageViewModel;
|
_vm = BindingContext as GroupingsPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.MainPage = mainPage;
|
_vm.MainPage = mainPage;
|
||||||
@@ -47,6 +50,7 @@ namespace Bit.App.Pages
|
|||||||
_vm.FolderId = folderId;
|
_vm.FolderId = folderId;
|
||||||
_vm.CollectionId = collectionId;
|
_vm.CollectionId = collectionId;
|
||||||
_vm.Deleted = deleted;
|
_vm.Deleted = deleted;
|
||||||
|
_vm.ShowTotp = showTotp;
|
||||||
_previousPage = previousPage;
|
_previousPage = previousPage;
|
||||||
if (pageTitle != null)
|
if (pageTitle != null)
|
||||||
{
|
{
|
||||||
@@ -68,7 +72,7 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_lockItem);
|
ToolbarItems.Add(_lockItem);
|
||||||
ToolbarItems.Add(_exitItem);
|
ToolbarItems.Add(_exitItem);
|
||||||
}
|
}
|
||||||
if (deleted)
|
if (deleted || showTotp)
|
||||||
{
|
{
|
||||||
_absLayout.Children.Remove(_fab);
|
_absLayout.Children.Remove(_fab);
|
||||||
ToolbarItems.Remove(_addItem);
|
ToolbarItems.Remove(_addItem);
|
||||||
@@ -95,21 +99,28 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "syncStarted")
|
try
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
if (message.Command == "syncStarted")
|
||||||
}
|
|
||||||
else if (message.Command == "syncCompleted")
|
|
||||||
{
|
|
||||||
await Task.Delay(500);
|
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||||
if (_vm.LoadedOnce)
|
}
|
||||||
|
else if (message.Command == "syncCompleted")
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
var task = _vm.LoadAsync();
|
IsBusy = false;
|
||||||
}
|
if (_vm.LoadedOnce)
|
||||||
});
|
{
|
||||||
|
var task = _vm.LoadAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,10 +192,11 @@ namespace Bit.App.Pages
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDisappearing()
|
protected override async void OnDisappearing()
|
||||||
{
|
{
|
||||||
base.OnDisappearing();
|
base.OnDisappearing();
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
|
_vm.StopCiphersTotpTick().FireAndForget();
|
||||||
_broadcasterService.Unsubscribe(_pageName);
|
_broadcasterService.Unsubscribe(_pageName);
|
||||||
_vm.DisableRefreshing();
|
_vm.DisableRefreshing();
|
||||||
_accountAvatar?.OnDisappearing();
|
_accountAvatar?.OnDisappearing();
|
||||||
@@ -192,35 +204,54 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
try
|
||||||
if (!DoOnce())
|
|
||||||
{
|
{
|
||||||
return;
|
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||||
}
|
if (!DoOnce())
|
||||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
{
|
||||||
{
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (item.IsTrash)
|
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
|
||||||
{
|
{
|
||||||
await _vm.SelectTrashAsync();
|
await _vm.SelectCipherAsync(totpItem.Cipher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.IsTrash)
|
||||||
|
{
|
||||||
|
await _vm.SelectTrashAsync();
|
||||||
|
}
|
||||||
|
else if (item.IsTotpCode)
|
||||||
|
{
|
||||||
|
await _vm.SelectTotpCodesAsync();
|
||||||
|
}
|
||||||
|
else if (item.Cipher != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectCipherAsync(item.Cipher);
|
||||||
|
}
|
||||||
|
else if (item.Folder != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectFolderAsync(item.Folder);
|
||||||
|
}
|
||||||
|
else if (item.Collection != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectCollectionAsync(item.Collection);
|
||||||
|
}
|
||||||
|
else if (item.Type != null)
|
||||||
|
{
|
||||||
|
await _vm.SelectTypeAsync(item.Type.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (item.Cipher != null)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await _vm.SelectCipherAsync(item.Cipher);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
_platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||||
else if (item.Folder != null)
|
|
||||||
{
|
|
||||||
await _vm.SelectFolderAsync(item.Folder);
|
|
||||||
}
|
|
||||||
else if (item.Collection != null)
|
|
||||||
{
|
|
||||||
await _vm.SelectCollectionAsync(item.Collection);
|
|
||||||
}
|
|
||||||
else if (item.Type != null)
|
|
||||||
{
|
|
||||||
await _vm.SelectTypeAsync(item.Type.Value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +294,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (!_vm.Deleted && DoOnce())
|
if (!_vm.Deleted && DoOnce())
|
||||||
{
|
{
|
||||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
var page = new CipherAddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,11 +308,11 @@ namespace Bit.App.Pages
|
|||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||||
{
|
{
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId)));
|
await Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(_previousPage.CipherId)));
|
||||||
}
|
}
|
||||||
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||||
{
|
{
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
|
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_previousPage.CipherId)));
|
||||||
}
|
}
|
||||||
_previousPage = null;
|
_previousPage = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class GroupingsPageListGroup : List<GroupingsPageListItem>
|
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
|
||||||
{
|
{
|
||||||
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||||
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
|
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
|
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
|
||||||
bool doUpper = true, bool first = false)
|
bool doUpper = true, bool first = false)
|
||||||
{
|
{
|
||||||
AddRange(groupItems);
|
AddRange(groupItems);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace Bit.App.Pages
|
|||||||
public string ItemCount { get; set; }
|
public string ItemCount { get; set; }
|
||||||
public bool FuzzyAutofill { get; set; }
|
public bool FuzzyAutofill { get; set; }
|
||||||
public bool IsTrash { get; set; }
|
public bool IsTrash { get; set; }
|
||||||
|
public bool IsTotpCode { get; set; }
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,10 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_name = Collection.Name;
|
_name = Collection.Name;
|
||||||
}
|
}
|
||||||
|
else if (IsTotpCode)
|
||||||
|
{
|
||||||
|
_name = AppResources.VerificationCodes;
|
||||||
|
}
|
||||||
else if (Type != null)
|
else if (Type != null)
|
||||||
{
|
{
|
||||||
switch (Type.Value)
|
switch (Type.Value)
|
||||||
@@ -82,6 +87,10 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_icon = BitwardenIcons.Collection;
|
_icon = BitwardenIcons.Collection;
|
||||||
}
|
}
|
||||||
|
else if (IsTotpCode)
|
||||||
|
{
|
||||||
|
_icon = BitwardenIcons.Clock;
|
||||||
|
}
|
||||||
else if (Type != null)
|
else if (Type != null)
|
||||||
{
|
{
|
||||||
switch (Type.Value)
|
switch (Type.Value)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Bit.App.Pages
|
|||||||
public DataTemplate HeaderTemplate { get; set; }
|
public DataTemplate HeaderTemplate { get; set; }
|
||||||
public DataTemplate CipherTemplate { get; set; }
|
public DataTemplate CipherTemplate { get; set; }
|
||||||
public DataTemplate GroupTemplate { get; set; }
|
public DataTemplate GroupTemplate { get; set; }
|
||||||
|
public DataTemplate AuthenticatorTemplate { get; set; }
|
||||||
|
|
||||||
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||||
{
|
{
|
||||||
@@ -15,10 +16,16 @@ namespace Bit.App.Pages
|
|||||||
return HeaderTemplate;
|
return HeaderTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item is GroupingsPageTOTPListItem)
|
||||||
|
{
|
||||||
|
return AuthenticatorTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
if (item is GroupingsPageListItem listItem)
|
if (item is GroupingsPageListItem listItem)
|
||||||
{
|
{
|
||||||
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
|
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem
|
||||||
|
{
|
||||||
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
private readonly ITotpService _totpService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private readonly IClipboardService _clipboardService;
|
||||||
|
private CipherView _cipher;
|
||||||
|
|
||||||
|
private bool _websiteIconsEnabled;
|
||||||
|
private string _iconImageSource = string.Empty;
|
||||||
|
|
||||||
|
public int interval { get; set; }
|
||||||
|
private double _progress;
|
||||||
|
private string _totpSec;
|
||||||
|
private string _totpCodeFormatted;
|
||||||
|
private TotpHelper _totpTickHelper;
|
||||||
|
|
||||||
|
|
||||||
|
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
|
||||||
|
{
|
||||||
|
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
|
|
||||||
|
Cipher = cipherView;
|
||||||
|
WebsiteIconsEnabled = websiteIconsEnabled;
|
||||||
|
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
|
||||||
|
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
|
||||||
|
onException: ex => _logger.Value.Exception(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
_totpTickHelper = new TotpHelper(cipherView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncCommand CopyCommand { get; set; }
|
||||||
|
|
||||||
|
public CipherView Cipher
|
||||||
|
{
|
||||||
|
get => _cipher;
|
||||||
|
set => SetProperty(ref _cipher, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string TotpCodeFormatted
|
||||||
|
{
|
||||||
|
get => _totpCodeFormatted;
|
||||||
|
set => SetProperty(ref _totpCodeFormatted, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(TotpCodeFormattedStart),
|
||||||
|
nameof(TotpCodeFormattedEnd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public string TotpSec
|
||||||
|
{
|
||||||
|
get => _totpSec;
|
||||||
|
set => SetProperty(ref _totpSec, value);
|
||||||
|
}
|
||||||
|
public double Progress
|
||||||
|
{
|
||||||
|
get => _progress;
|
||||||
|
set => SetProperty(ref _progress, value);
|
||||||
|
}
|
||||||
|
public bool WebsiteIconsEnabled
|
||||||
|
{
|
||||||
|
get => _websiteIconsEnabled;
|
||||||
|
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowIconImage
|
||||||
|
{
|
||||||
|
get => WebsiteIconsEnabled
|
||||||
|
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||||
|
&& IconImageSource != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string IconImageSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||||
|
{
|
||||||
|
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||||
|
}
|
||||||
|
return _iconImageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
|
||||||
|
|
||||||
|
public string TotpCodeFormattedEnd => TotpCodeFormatted?.Split(' ')[1];
|
||||||
|
|
||||||
|
public async Task CopyToClipboardAsync()
|
||||||
|
{
|
||||||
|
await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty));
|
||||||
|
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task TotpTickAsync()
|
||||||
|
{
|
||||||
|
await _totpTickHelper.GenerateNewTotpValues();
|
||||||
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
TotpSec = _totpTickHelper.TotpSec;
|
||||||
|
Progress = _totpTickHelper.Progress;
|
||||||
|
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
@@ -17,7 +18,7 @@ using Xamarin.Forms;
|
|||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class GroupingsPageViewModel : BaseViewModel
|
public class GroupingsPageViewModel : VaultFilterViewModel
|
||||||
{
|
{
|
||||||
private const int NoFolderListSize = 100;
|
private const int NoFolderListSize = 100;
|
||||||
|
|
||||||
@@ -30,16 +31,16 @@ namespace Bit.App.Pages
|
|||||||
private bool _showList;
|
private bool _showList;
|
||||||
private bool _websiteIconsEnabled;
|
private bool _websiteIconsEnabled;
|
||||||
private bool _syncRefreshing;
|
private bool _syncRefreshing;
|
||||||
private bool _showVaultFilter;
|
private bool _showTotpFilter;
|
||||||
private string _vaultFilterSelection;
|
private bool _totpFilterEnable;
|
||||||
private string _noDataText;
|
private string _noDataText;
|
||||||
private List<Organization> _organizations;
|
|
||||||
private List<CipherView> _allCiphers;
|
private List<CipherView> _allCiphers;
|
||||||
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
||||||
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
||||||
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
||||||
private int _deletedCount = 0;
|
private int _deletedCount = 0;
|
||||||
|
private CancellationTokenSource _totpTickCts;
|
||||||
|
private Task _totpTickTask;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IFolderService _folderService;
|
private readonly IFolderService _folderService;
|
||||||
private readonly ICollectionService _collectionService;
|
private readonly ICollectionService _collectionService;
|
||||||
@@ -101,6 +102,7 @@ namespace Bit.App.Pages
|
|||||||
&& NoFolderCiphers.Count < NoFolderListSize
|
&& NoFolderCiphers.Count < NoFolderListSize
|
||||||
&& (Collections is null || !Collections.Any());
|
&& (Collections is null || !Collections.Any());
|
||||||
public List<CipherView> Ciphers { get; set; }
|
public List<CipherView> Ciphers { get; set; }
|
||||||
|
public List<CipherView> TOTPCiphers { get; set; }
|
||||||
public List<CipherView> FavoriteCiphers { get; set; }
|
public List<CipherView> FavoriteCiphers { get; set; }
|
||||||
public List<CipherView> NoFolderCiphers { get; set; }
|
public List<CipherView> NoFolderCiphers { get; set; }
|
||||||
public List<FolderView> Folders { get; set; }
|
public List<FolderView> Folders { get; set; }
|
||||||
@@ -108,6 +110,11 @@ namespace Bit.App.Pages
|
|||||||
public List<Core.Models.View.CollectionView> Collections { get; set; }
|
public List<Core.Models.View.CollectionView> Collections { get; set; }
|
||||||
public List<TreeNode<Core.Models.View.CollectionView>> NestedCollections { get; set; }
|
public List<TreeNode<Core.Models.View.CollectionView>> NestedCollections { get; set; }
|
||||||
|
|
||||||
|
protected override ICipherService cipherService => _cipherService;
|
||||||
|
protected override IPolicyService policyService => _policyService;
|
||||||
|
protected override IOrganizationService organizationService => _organizationService;
|
||||||
|
protected override ILogger logger => _logger;
|
||||||
|
|
||||||
public bool Refreshing
|
public bool Refreshing
|
||||||
{
|
{
|
||||||
get => _refreshing;
|
get => _refreshing;
|
||||||
@@ -153,30 +160,15 @@ namespace Bit.App.Pages
|
|||||||
get => _websiteIconsEnabled;
|
get => _websiteIconsEnabled;
|
||||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||||
}
|
}
|
||||||
public bool ShowVaultFilter
|
public bool ShowTotp
|
||||||
{
|
{
|
||||||
get => _showVaultFilter;
|
get => _showTotpFilter;
|
||||||
set => SetProperty(ref _showVaultFilter, value);
|
set => SetProperty(ref _showTotpFilter, value);
|
||||||
}
|
}
|
||||||
public string VaultFilterDescription
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
|
|
||||||
{
|
|
||||||
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
|
|
||||||
}
|
|
||||||
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
|
|
||||||
}
|
|
||||||
set => SetProperty(ref _vaultFilterSelection, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
||||||
public Command RefreshCommand { get; set; }
|
public Command RefreshCommand { get; set; }
|
||||||
public Command<CipherView> CipherOptionsCommand { get; set; }
|
public Command<CipherView> CipherOptionsCommand { get; set; }
|
||||||
public ICommand VaultFilterCommand { get; }
|
|
||||||
public bool LoadedOnce { get; set; }
|
public bool LoadedOnce { get; set; }
|
||||||
|
|
||||||
public async Task LoadAsync()
|
public async Task LoadAsync()
|
||||||
@@ -201,23 +193,20 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_organizations = await _organizationService.GetAllAsync();
|
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget();
|
||||||
|
|
||||||
|
await InitVaultFilterAsync(MainPage);
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync();
|
|
||||||
if (ShowVaultFilter && _vaultFilterSelection == null)
|
|
||||||
{
|
|
||||||
_vaultFilterSelection = AppResources.AllVaults;
|
|
||||||
}
|
|
||||||
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
|
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
|
||||||
}
|
}
|
||||||
|
|
||||||
_doingLoad = true;
|
_doingLoad = true;
|
||||||
LoadedOnce = true;
|
LoadedOnce = true;
|
||||||
ShowNoData = false;
|
ShowNoData = false;
|
||||||
Loading = true;
|
Loading = true;
|
||||||
ShowList = false;
|
ShowList = false;
|
||||||
ShowAddCipherButton = !Deleted;
|
ShowAddCipherButton = !Deleted;
|
||||||
|
|
||||||
var groupedItems = new List<GroupingsPageListGroup>();
|
var groupedItems = new List<GroupingsPageListGroup>();
|
||||||
var page = Page as GroupingsPage;
|
var page = Page as GroupingsPage;
|
||||||
|
|
||||||
@@ -241,6 +230,8 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
|
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
|
||||||
|
|
||||||
groupedItems.Add(new GroupingsPageListGroup(
|
groupedItems.Add(new GroupingsPageListGroup(
|
||||||
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
|
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
|
||||||
{
|
{
|
||||||
@@ -297,10 +288,12 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (Ciphers?.Any() ?? false)
|
if (Ciphers?.Any() ?? false)
|
||||||
{
|
{
|
||||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
|
CreateCipherGroupedItems(groupedItems);
|
||||||
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
}
|
||||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
if (ShowTotp && (!TOTPCiphers?.Any() ?? false))
|
||||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
{
|
||||||
|
Page.Navigation.PopAsync();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (ShowNoFolderCipherGroup)
|
if (ShowNoFolderCipherGroup)
|
||||||
{
|
{
|
||||||
@@ -388,39 +381,74 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddTotpGroupItem(List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
|
||||||
|
{
|
||||||
|
if (TOTPCiphers?.Any() == true)
|
||||||
|
{
|
||||||
|
groupedItems.Insert(0, new GroupingsPageListGroup(
|
||||||
|
AppResources.Totp, 1, uppercaseGroupNames, false)
|
||||||
|
{
|
||||||
|
new GroupingsPageListItem
|
||||||
|
{
|
||||||
|
IsTotpCode = true,
|
||||||
|
Type = CipherType.Login,
|
||||||
|
ItemCount = TOTPCiphers.Count().ToString("N0")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
|
||||||
|
{
|
||||||
|
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
||||||
|
_totpTickCts?.Cancel();
|
||||||
|
if (ShowTotp)
|
||||||
|
{
|
||||||
|
var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
|
||||||
|
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||||
|
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||||
|
|
||||||
|
StartCiphersTotpTick(ciphersListItems);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
|
||||||
|
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||||
|
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||||
|
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartCiphersTotpTick(List<GroupingsPageTOTPListItem> ciphersListItems)
|
||||||
|
{
|
||||||
|
_totpTickCts?.Cancel();
|
||||||
|
_totpTickCts = new CancellationTokenSource();
|
||||||
|
_totpTickTask = new TimerTask(logger, () => ciphersListItems.ForEach(i => i.TotpTickAsync()), _totpTickCts).RunPeriodic();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopCiphersTotpTick()
|
||||||
|
{
|
||||||
|
_totpTickCts?.Cancel();
|
||||||
|
if (_totpTickTask != null)
|
||||||
|
{
|
||||||
|
await _totpTickTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void DisableRefreshing()
|
public void DisableRefreshing()
|
||||||
{
|
{
|
||||||
Refreshing = false;
|
Refreshing = false;
|
||||||
SyncRefreshing = false;
|
SyncRefreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task VaultFilterOptionsAsync()
|
protected override async Task OnVaultFilterSelectedAsync()
|
||||||
{
|
{
|
||||||
var options = new List<string> { AppResources.AllVaults, AppResources.MyVault };
|
|
||||||
if (_organizations.Any())
|
|
||||||
{
|
|
||||||
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
|
|
||||||
}
|
|
||||||
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
|
|
||||||
options.ToArray());
|
|
||||||
if (selection == null || selection == AppResources.Cancel ||
|
|
||||||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
|
|
||||||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
VaultFilterDescription = selection;
|
|
||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetVaultFilterOrgId()
|
|
||||||
{
|
|
||||||
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SelectCipherAsync(CipherView cipher)
|
public async Task SelectCipherAsync(CipherView cipher)
|
||||||
{
|
{
|
||||||
var page = new ViewPage(cipher.Id);
|
var page = new CipherDetailsPage(cipher.Id);
|
||||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +495,13 @@ namespace Bit.App.Pages
|
|||||||
await Page.Navigation.PushAsync(page);
|
await Page.Navigation.PushAsync(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SelectTotpCodesAsync()
|
||||||
|
{
|
||||||
|
var page = new GroupingsPage(false, CipherType.Login, null, null, AppResources.VerificationCodes, _vaultFilterSelection, null,
|
||||||
|
false, true);
|
||||||
|
await Page.Navigation.PushAsync(page);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ExitAsync()
|
public async Task ExitAsync()
|
||||||
{
|
{
|
||||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
||||||
@@ -501,9 +536,11 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task LoadDataAsync()
|
private async Task LoadDataAsync()
|
||||||
{
|
{
|
||||||
var orgId = await FillAllCiphersAndGetOrgIdIfNeededAsync();
|
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
NoDataText = AppResources.NoItems;
|
NoDataText = AppResources.NoItems;
|
||||||
|
_allCiphers = await GetAllCiphersAsync();
|
||||||
HasCiphers = _allCiphers.Any();
|
HasCiphers = _allCiphers.Any();
|
||||||
|
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp) && (c.OrganizationUseTotp || canAccessPremium)).ToList();
|
||||||
FavoriteCiphers?.Clear();
|
FavoriteCiphers?.Clear();
|
||||||
NoFolderCiphers?.Clear();
|
NoFolderCiphers?.Clear();
|
||||||
_folderCounts.Clear();
|
_folderCounts.Clear();
|
||||||
@@ -516,7 +553,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
await FillFoldersAndCollectionsAsync(orgId);
|
await FillFoldersAndCollectionsAsync();
|
||||||
NestedFolders = await _folderService.GetAllNestedAsync(Folders);
|
NestedFolders = await _folderService.GetAllNestedAsync(Folders);
|
||||||
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
|
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
|
||||||
NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null;
|
NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null;
|
||||||
@@ -529,6 +566,10 @@ namespace Bit.App.Pages
|
|||||||
Filter = c => c.IsDeleted;
|
Filter = c => c.IsDeleted;
|
||||||
NoDataText = AppResources.NoItemsTrash;
|
NoDataText = AppResources.NoItemsTrash;
|
||||||
}
|
}
|
||||||
|
else if (ShowTotp)
|
||||||
|
{
|
||||||
|
Filter = c => c.Type == CipherType.Login && !c.IsDeleted && !string.IsNullOrEmpty(c.Login?.Totp);
|
||||||
|
}
|
||||||
else if (Type != null)
|
else if (Type != null)
|
||||||
{
|
{
|
||||||
Filter = c => c.Type == Type.Value && !c.IsDeleted;
|
Filter = c => c.Type == Type.Value && !c.IsDeleted;
|
||||||
@@ -640,28 +681,9 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> FillAllCiphersAndGetOrgIdIfNeededAsync()
|
private async Task FillFoldersAndCollectionsAsync()
|
||||||
{
|
|
||||||
string orgId = null;
|
|
||||||
var decCiphers = await _cipherService.GetAllDecryptedAsync();
|
|
||||||
if (IsVaultFilterMyVault)
|
|
||||||
{
|
|
||||||
_allCiphers = decCiphers.Where(c => c.OrganizationId == null).ToList();
|
|
||||||
}
|
|
||||||
else if (IsVaultFilterOrgVault)
|
|
||||||
{
|
|
||||||
orgId = GetVaultFilterOrgId();
|
|
||||||
_allCiphers = decCiphers.Where(c => c.OrganizationId == orgId).ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_allCiphers = decCiphers;
|
|
||||||
}
|
|
||||||
return orgId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FillFoldersAndCollectionsAsync(string orgId)
|
|
||||||
{
|
{
|
||||||
|
var orgId = GetVaultFilterOrgId();
|
||||||
var decFolders = await _folderService.GetAllDecryptedAsync();
|
var decFolders = await _folderService.GetAllDecryptedAsync();
|
||||||
var decCollections = await _collectionService.GetAllDecryptedAsync();
|
var decCollections = await _collectionService.GetAllDecryptedAsync();
|
||||||
if (IsVaultFilterMyVault)
|
if (IsVaultFilterMyVault)
|
||||||
@@ -681,11 +703,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
|
|
||||||
|
|
||||||
private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
|
|
||||||
_vaultFilterSelection != AppResources.MyVault;
|
|
||||||
|
|
||||||
private List<FolderView> BuildFolders(List<FolderView> decFolders)
|
private List<FolderView> BuildFolders(List<FolderView> decFolders)
|
||||||
{
|
{
|
||||||
var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList();
|
var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList();
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<pages:BaseContentPage
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.ScanPage"
|
x:Class="Bit.App.Pages.ScanPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
|
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
|
||||||
x:Name="_page"
|
x:Name="_page"
|
||||||
Title="{u:I18n ScanQrTitle}">
|
Title="{Binding ScanQrPageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:ScanPageViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
@@ -16,67 +29,114 @@
|
|||||||
<Grid
|
<Grid
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
HorizontalOptions="FillAndExpand">
|
HorizontalOptions="FillAndExpand">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
<zxing:ZXingScannerView
|
<zxing:ZXingScannerView
|
||||||
x:Name="_zxing"
|
x:Name="_zxing"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
AutomationId="zxingScannerView"
|
AutomationId="zxingScannerView"
|
||||||
OnScanResult="OnScanResult">
|
IsVisible="{Binding ShowScanner}"
|
||||||
</zxing:ZXingScannerView>
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
<Grid
|
Grid.RowSpan="3"
|
||||||
VerticalOptions="FillAndExpand"
|
OnScanResult="OnScanResult"/>
|
||||||
|
<StackLayout
|
||||||
|
VerticalOptions="Center"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
AutomationId="zxingDefaultOverlay">
|
IsVisible="{Binding ShowScanner}"
|
||||||
<Grid.RowDefinitions>
|
Grid.Column="0"
|
||||||
<RowDefinition Height="*" />
|
Grid.Row="0"
|
||||||
<RowDefinition Height="*" />
|
Grid.RowSpan="2"
|
||||||
<RowDefinition Height="*" />
|
Margin="30,0">
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<BoxView
|
<forms:SKCanvasView
|
||||||
Grid.Column="0"
|
x:Name="SkCanvasView"
|
||||||
Grid.Row="0"
|
Margin="0,50,0,0"
|
||||||
VerticalOptions="Fill"
|
WidthRequest="250"
|
||||||
HorizontalOptions="FillAndExpand"
|
HeightRequest="250"
|
||||||
BackgroundColor="Black"
|
IsVisible="{Binding ShowScanner}"
|
||||||
Opacity="0.7" />
|
VerticalOptions="Center"
|
||||||
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n CameraInstructionTop}"
|
|
||||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.Row="0"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
TextColor="White" />
|
PaintSurface="OnCanvasViewPaintSurface"/>
|
||||||
|
|
||||||
<BoxView
|
<controls:IconButton
|
||||||
Grid.Column="0"
|
x:Name="_checkIcon"
|
||||||
Grid.Row="1"
|
IsVisible="{Binding ShowScanner}"
|
||||||
VerticalOptions="Fill"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Start"
|
||||||
|
FontSize="Title"
|
||||||
|
TextColor="Transparent"/>
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||||
|
BackgroundColor="{DynamicResource BackgroundColor}"/>
|
||||||
|
<StackLayout
|
||||||
|
VerticalOptions="Center"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
BackgroundColor="Transparent" />
|
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||||
|
Grid.Column="0"
|
||||||
<BoxView
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Margin="30,0">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n EnterKeyManually}"
|
||||||
|
FontSize="Title" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n AuthenticatorKeyScanner}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<controls:MonoEntry
|
||||||
|
x:Name="_authenticationKeyEntry"
|
||||||
|
Text="{Binding TotpAuthenticationKey}"
|
||||||
|
IsSpellCheckEnabled="False"
|
||||||
|
IsTextPredictionEnabled="False"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n AddTotp}"
|
||||||
|
StyleClass="box-button-row"
|
||||||
|
Clicked="AddAuthenticationKey_OnClicked"/>
|
||||||
|
</StackLayout>
|
||||||
|
<BoxView
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
VerticalOptions="Fill"
|
VerticalOptions="Fill"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
BackgroundColor="Black"
|
BackgroundColor="Black"
|
||||||
Opacity="0.7" />
|
Opacity="0.7" />
|
||||||
|
<StackLayout
|
||||||
|
VerticalOptions="Start"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="2">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n CameraInstructionBottom}"
|
Text="{Binding CameraInstructionTop}"
|
||||||
AutomationId="zxingDefaultOverlay_BottomTextLabel"
|
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||||
|
Margin="30,15,30,0"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
TextColor="White" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label
|
||||||
|
FormattedText="{Binding ToggleScanModeLabel}"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
VerticalOptions="Center"
|
Margin="0,15"
|
||||||
HorizontalOptions="Center"
|
StyleClass="text-sm"
|
||||||
TextColor="White" />
|
FontAttributes="Bold"
|
||||||
</Grid>
|
VerticalOptions="End"
|
||||||
|
HorizontalOptions="Center" >
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</pages:BaseContentPage>
|
||||||
</pages:BaseContentPage>
|
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.Views.Forms;
|
||||||
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class ScanPage : BaseContentPage
|
public partial class ScanPage : BaseContentPage
|
||||||
{
|
{
|
||||||
|
private ScanPageViewModel ViewModel => BindingContext as ScanPageViewModel;
|
||||||
private readonly Action<string> _callback;
|
private readonly Action<string> _callback;
|
||||||
|
|
||||||
private CancellationTokenSource _autofocusCts;
|
private CancellationTokenSource _autofocusCts;
|
||||||
private Task _continuousAutofocusTask;
|
private Task _continuousAutofocusTask;
|
||||||
|
private readonly Color _greenColor;
|
||||||
|
private readonly SKColor _blueSKColor;
|
||||||
|
private readonly SKColor _greenSKColor;
|
||||||
|
private readonly Stopwatch _stopwatch;
|
||||||
|
private bool _pageIsActive;
|
||||||
|
private bool _qrcodeFound;
|
||||||
|
private float _scale;
|
||||||
|
|
||||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
@@ -32,6 +44,12 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
ToolbarItems.RemoveAt(0);
|
ToolbarItems.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_greenColor = ThemeManager.GetResourceColor("SuccessColor");
|
||||||
|
_greenSKColor = _greenColor.ToSKColor();
|
||||||
|
_blueSKColor = ThemeManager.GetResourceColor("PrimaryColor").ToSKColor();
|
||||||
|
_stopwatch = new Stopwatch();
|
||||||
|
_qrcodeFound = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnAppearing()
|
protected override void OnAppearing()
|
||||||
@@ -58,7 +76,14 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (!autofocusCts.IsCancellationRequested)
|
if (!autofocusCts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_zxing.AutoFocus();
|
try
|
||||||
|
{
|
||||||
|
_zxing.AutoFocus();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,49 +94,70 @@ namespace Bit.App.Pages
|
|||||||
_logger.Value.Exception(ex);
|
_logger.Value.Exception(ex);
|
||||||
}
|
}
|
||||||
}, autofocusCts.Token);
|
}, autofocusCts.Token);
|
||||||
|
_pageIsActive = true;
|
||||||
|
AnimationLoopAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnDisappearing()
|
protected override async void OnDisappearing()
|
||||||
{
|
{
|
||||||
_autofocusCts?.Cancel();
|
_autofocusCts?.Cancel();
|
||||||
|
|
||||||
if (_continuousAutofocusTask != null)
|
if (_continuousAutofocusTask != null)
|
||||||
{
|
{
|
||||||
await _continuousAutofocusTask;
|
await _continuousAutofocusTask;
|
||||||
}
|
}
|
||||||
_zxing.IsScanning = false;
|
_zxing.IsScanning = false;
|
||||||
|
_pageIsActive = false;
|
||||||
base.OnDisappearing();
|
base.OnDisappearing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnScanResult(ZXing.Result result)
|
private async void OnScanResult(ZXing.Result result)
|
||||||
{
|
{
|
||||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
try
|
||||||
_zxing.IsAnalyzing = false;
|
|
||||||
_zxing.IsScanning = false;
|
|
||||||
var text = result?.Text;
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
{
|
||||||
if (text.StartsWith("otpauth://totp"))
|
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||||
|
_zxing.IsAnalyzing = false;
|
||||||
|
var text = result?.Text;
|
||||||
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
_callback(text);
|
if (text.StartsWith("otpauth://totp"))
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
|
||||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
|
||||||
{
|
|
||||||
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
|
||||||
foreach (var part in queryParts)
|
|
||||||
{
|
{
|
||||||
if (part.StartsWith("secret="))
|
await QrCodeFoundAsync();
|
||||||
|
_callback(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
||||||
|
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||||
|
{
|
||||||
|
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
||||||
|
foreach (var part in queryParts)
|
||||||
{
|
{
|
||||||
_callback(part.Substring(7)?.ToUpperInvariant());
|
if (part.StartsWith("secret="))
|
||||||
return;
|
{
|
||||||
|
await QrCodeFoundAsync();
|
||||||
|
var subResult = part.Substring(7);
|
||||||
|
if (!string.IsNullOrEmpty(subResult))
|
||||||
|
{
|
||||||
|
_callback(subResult.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_callback(null);
|
||||||
}
|
}
|
||||||
_callback(null);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.Value?.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task QrCodeFoundAsync()
|
||||||
|
{
|
||||||
|
_qrcodeFound = true;
|
||||||
|
Vibration.Vibrate();
|
||||||
|
await Task.Delay(1000);
|
||||||
|
_zxing.IsScanning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
@@ -121,5 +167,89 @@ namespace Bit.App.Pages
|
|||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(ViewModel.TotpAuthenticationKey))
|
||||||
|
{
|
||||||
|
_callback(ViewModel.TotpAuthenticationKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.ToggleScanModeCommand.Execute(null);
|
||||||
|
if (!ViewModel.ShowScanner)
|
||||||
|
{
|
||||||
|
_authenticationKeyEntry.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
|
||||||
|
{
|
||||||
|
var info = args.Info;
|
||||||
|
var surface = args.Surface;
|
||||||
|
var canvas = surface.Canvas;
|
||||||
|
var margins = 20;
|
||||||
|
var maxSquareSize = (Math.Min(info.Height, info.Width) * 0.9f - margins) * _scale;
|
||||||
|
var squareSize = maxSquareSize;
|
||||||
|
var lineSize = squareSize * 0.15f;
|
||||||
|
var startXPoint = (info.Width / 2) - (squareSize / 2);
|
||||||
|
var startYPoint = (info.Height / 2) - (squareSize / 2);
|
||||||
|
canvas.Clear(SKColors.Transparent);
|
||||||
|
|
||||||
|
using (var strokePaint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = _qrcodeFound ? _greenSKColor : _blueSKColor,
|
||||||
|
StrokeWidth = 9 * _scale,
|
||||||
|
StrokeCap = SKStrokeCap.Round,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
canvas.Scale(1, 1);
|
||||||
|
//top left
|
||||||
|
canvas.DrawLine(startXPoint, startYPoint, startXPoint, startYPoint + lineSize, strokePaint);
|
||||||
|
canvas.DrawLine(startXPoint, startYPoint, startXPoint + lineSize, startYPoint, strokePaint);
|
||||||
|
//bot left
|
||||||
|
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint, startYPoint + squareSize - lineSize, strokePaint);
|
||||||
|
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint + lineSize, startYPoint + squareSize, strokePaint);
|
||||||
|
//top right
|
||||||
|
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize - lineSize, startYPoint, strokePaint);
|
||||||
|
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize, startYPoint + lineSize, strokePaint);
|
||||||
|
//bot right
|
||||||
|
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize - lineSize, startYPoint + squareSize, strokePaint);
|
||||||
|
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task AnimationLoopAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stopwatch.Start();
|
||||||
|
while (_pageIsActive)
|
||||||
|
{
|
||||||
|
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
|
||||||
|
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
|
||||||
|
SkCanvasView.InvalidateSurface();
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
|
||||||
|
if (_qrcodeFound && _scale > 0.98f)
|
||||||
|
{
|
||||||
|
_checkIcon.TextColor = _greenColor;
|
||||||
|
SkCanvasView.InvalidateSurface();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger?.Value?.Exception(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_stopwatch?.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/App/Pages/Vault/ScanPageViewModel.cs
Normal file
61
src/App/Pages/Vault/ScanPageViewModel.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class ScanPageViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private bool _showScanner = true;
|
||||||
|
private string _totpAuthenticationKey;
|
||||||
|
|
||||||
|
public ScanPageViewModel()
|
||||||
|
{
|
||||||
|
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Command ToggleScanModeCommand { get; set; }
|
||||||
|
public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
|
||||||
|
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
|
||||||
|
public string TotpAuthenticationKey
|
||||||
|
{
|
||||||
|
get => _totpAuthenticationKey;
|
||||||
|
set => SetProperty(ref _totpAuthenticationKey, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(ToggleScanModeLabel)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public bool ShowScanner
|
||||||
|
{
|
||||||
|
get => _showScanner;
|
||||||
|
set => SetProperty(ref _showScanner, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(ToggleScanModeLabel),
|
||||||
|
nameof(ScanQrPageTitle),
|
||||||
|
nameof(CameraInstructionTop)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public FormattedString ToggleScanModeLabel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var fs = new FormattedString();
|
||||||
|
fs.Spans.Add(new Span
|
||||||
|
{
|
||||||
|
Text = ShowScanner ? AppResources.CannotScanQRCode : AppResources.CannotAddAuthenticatorKey,
|
||||||
|
TextColor = ThemeManager.GetResourceColor("TitleTextColor")
|
||||||
|
});
|
||||||
|
fs.Spans.Add(new Span
|
||||||
|
{
|
||||||
|
Text = ShowScanner ? AppResources.EnterKeyManually : AppResources.ScanQRCode,
|
||||||
|
TextColor = ThemeManager.GetResourceColor("ScanningToggleModeTextColor")
|
||||||
|
});
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/App/Pages/VaultFilterViewModel.cs
Normal file
123
src/App/Pages/VaultFilterViewModel.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public abstract class VaultFilterViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
protected abstract ICipherService cipherService { get; }
|
||||||
|
protected abstract IPolicyService policyService { get; }
|
||||||
|
protected abstract IOrganizationService organizationService { get; }
|
||||||
|
protected abstract ILogger logger { get; }
|
||||||
|
|
||||||
|
protected bool _showVaultFilter;
|
||||||
|
protected bool _personalOwnershipPolicyApplies;
|
||||||
|
protected string _vaultFilterSelection;
|
||||||
|
protected List<Organization> _organizations;
|
||||||
|
|
||||||
|
public VaultFilterViewModel()
|
||||||
|
{
|
||||||
|
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
|
||||||
|
onException: ex => logger.Exception(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand VaultFilterCommand { get; set; }
|
||||||
|
|
||||||
|
public bool ShowVaultFilter
|
||||||
|
{
|
||||||
|
get => _showVaultFilter;
|
||||||
|
set => SetProperty(ref _showVaultFilter, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string VaultFilterDescription
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
|
||||||
|
{
|
||||||
|
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
|
||||||
|
}
|
||||||
|
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
|
||||||
|
}
|
||||||
|
set => SetProperty(ref _vaultFilterSelection, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetVaultFilterOrgId()
|
||||||
|
{
|
||||||
|
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
|
||||||
|
|
||||||
|
protected bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
|
||||||
|
_vaultFilterSelection != AppResources.MyVault;
|
||||||
|
|
||||||
|
protected async Task InitVaultFilterAsync(bool shouldUpdateShowVaultFilter)
|
||||||
|
{
|
||||||
|
_organizations = await organizationService.GetAllAsync();
|
||||||
|
if (_organizations?.Any() ?? false)
|
||||||
|
{
|
||||||
|
_personalOwnershipPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.PersonalOwnership);
|
||||||
|
var singleOrgPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.OnlyOrg);
|
||||||
|
if (_vaultFilterSelection == null || (_personalOwnershipPolicyApplies && singleOrgPolicyApplies))
|
||||||
|
{
|
||||||
|
VaultFilterDescription = AppResources.AllVaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldUpdateShowVaultFilter)
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
ShowVaultFilter = await policyService.ShouldShowVaultFilterAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<List<CipherView>> GetAllCiphersAsync()
|
||||||
|
{
|
||||||
|
var decCiphers = await cipherService.GetAllDecryptedAsync();
|
||||||
|
if (IsVaultFilterMyVault)
|
||||||
|
{
|
||||||
|
return decCiphers.Where(c => c.OrganizationId == null).ToList();
|
||||||
|
}
|
||||||
|
if (IsVaultFilterOrgVault)
|
||||||
|
{
|
||||||
|
var orgId = GetVaultFilterOrgId();
|
||||||
|
return decCiphers.Where(c => c.OrganizationId == orgId).ToList();
|
||||||
|
}
|
||||||
|
return decCiphers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task VaultFilterOptionsAsync()
|
||||||
|
{
|
||||||
|
var options = new List<string> { AppResources.AllVaults };
|
||||||
|
if (!_personalOwnershipPolicyApplies)
|
||||||
|
{
|
||||||
|
options.Add(AppResources.MyVault);
|
||||||
|
}
|
||||||
|
if (_organizations.Any())
|
||||||
|
{
|
||||||
|
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
|
||||||
|
}
|
||||||
|
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
|
||||||
|
options.ToArray());
|
||||||
|
if (selection == null || selection == AppResources.Cancel ||
|
||||||
|
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
|
||||||
|
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VaultFilterDescription = selection;
|
||||||
|
await OnVaultFilterSelectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task OnVaultFilterSelectedAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
9267
src/App/Resources/AppResources.Designer.cs
generated
9267
src/App/Resources/AppResources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -276,16 +276,16 @@
|
|||||||
<value>Is u seker u wil uitteken?</value>
|
<value>Is u seker u wil uitteken?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveAccount" xml:space="preserve">
|
<data name="RemoveAccount" xml:space="preserve">
|
||||||
<value>Remove Account</value>
|
<value>Verwyder rekening</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveAccountConfirmation" xml:space="preserve">
|
<data name="RemoveAccountConfirmation" xml:space="preserve">
|
||||||
<value>Are you sure you want to remove this account?</value>
|
<value>Is u seker u wil hierdie rekening verwyder?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountAlreadyAdded" xml:space="preserve">
|
<data name="AccountAlreadyAdded" xml:space="preserve">
|
||||||
<value>Account Already Added</value>
|
<value>Rekening reeds toegevoeg</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
|
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
|
||||||
<value>Would you like to switch to it now?</value>
|
<value>Wil u nou daarheen wissel?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MasterPassword" xml:space="preserve">
|
<data name="MasterPassword" xml:space="preserve">
|
||||||
<value>Hoofwagwoord</value>
|
<value>Hoofwagwoord</value>
|
||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>My kluis</value>
|
<value>My kluis</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Naam</value>
|
<value>Naam</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Geaktiveer</value>
|
<value>Geaktiveer</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Af</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>Aan</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Kan nie waarmerksleutel lees nie.</value>
|
<value>Kan nie waarmerksleutel lees nie.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Skandering gebeur outomaties.</value>
|
<value>Point your camera at the QR Code.
|
||||||
</data>
|
Scanning will happen automatically.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Rig u kamera op die QR-kode.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Skandeer QR-kode</value>
|
<value>Skandeer QR-kode</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Kopieer TOTP</value>
|
<value>Kopieer TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Indien u aantekening aan ’n bevestigingskode gekoppel is, word die TOTP-bevestigingskode outomaties na die knipbord gekopieer by die aantekening se outovul.</value>
|
<value>Indien ’n aantekening ’n waarmerksleutel het, kopieer die TOTP-bevestigingskope na u knipbord wanneer u die aantekening outo-invul.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Deaktiveer outomatiese kopieëring van TOTP</value>
|
<value>Kopieer TOTP outomaties</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>’n Premie-lidmaatskap is nodig om hierdie funksie te gebruik.</value>
|
<value>’n Premie-lidmaatskap is nodig om hierdie funksie te gebruik.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Vervaldatum</value>
|
<value>Vervaldatum</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Deaktiveer webwerfikone</value>
|
<value>Toon webwerfikone</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Webwerfikone verskaf ’n herkenbare beeld langs elke aantekenitem in u kluis.</value>
|
<value>Toon ’n herkenbare prent langs elke aantekening.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>Ikoonbedienerbronadres</value>
|
<value>Ikoonbedienerbronadres</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Deursoek versameling</value>
|
<value>Deursoek versameling</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Deursoek vouer</value>
|
<value>Deursoek lêer-Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Deursoek tipe</value>
|
<value>Deursoek teks-Sends</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Soek {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Tipe</value>
|
<value>Tipe</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Verstek (Stelsel)</value>
|
<value>Verstek (Stelsel)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Verstekdonkertema</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Kies welke donkertema om te gebruik as stelseltema wanneer u toestel se donkertema geaktiveer is.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Kopieer notas</value>
|
<value>Kopieer notas</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Swart</value>
|
<value>Swart</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Versperde URI’s</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>URI’s op die swartlys word nie outomaties ingevul nie. Die lys moet met komma’s geskei wees. Bv. “https://twitter.com, androidapp://com.twitter.android”.</value>
|
<value>Versperde URI’s vir outovul</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Deaktiveer bewaarpor</value>
|
<value>Outovul sal nie vir versperde URI’s gebied word nie. Skei meerdere URI’s met ’n komma. Byvoorbeeld: “https://twitter.com, androidapp://com.twitter.android”.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>Die “bewaarpor” vra u outomaties om nuwe items in u kluis te bewaar wanneer u dit vir die eerste maal invoer.</value>
|
<value>Vra om aantekening toe te voeg</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Vra om ’n item toe te voeg indien een nie in u kluis gevind word nie.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Nadat toep herbegin is</value>
|
<value>Nadat toep herbegin is</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>’n Vriendelike naam om hierdie Send te beskryf.</value>
|
<value>’n Vriendelike naam om hierdie Send te beskryf.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Teks</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Teks</value>
|
<value>Teks</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Die lêer wat u wil verstuur.</value>
|
<value>Die lêer wat u wil verstuur.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Lêertipe is gekies.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Lêertipe is nie gekies nie, tik om te kies.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Tekstipe is gekies.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Tekstipe is nie gekies nie, tik om te kies.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Skrapdatum</value>
|
<value>Skrapdatum</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2105,28 +2142,28 @@
|
|||||||
<value>Een of meer organisasiebeleide verhoed u om u persoonlike kluis uit te stuur.</value>
|
<value>Een of meer organisasiebeleide verhoed u om u persoonlike kluis uit te stuur.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AddAccount" xml:space="preserve">
|
<data name="AddAccount" xml:space="preserve">
|
||||||
<value>Add Account</value>
|
<value>Voeg rekening toe</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountUnlocked" xml:space="preserve">
|
<data name="AccountUnlocked" xml:space="preserve">
|
||||||
<value>Unlocked</value>
|
<value>Ontgrendel</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountLocked" xml:space="preserve">
|
<data name="AccountLocked" xml:space="preserve">
|
||||||
<value>Locked</value>
|
<value>Vergrendel</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountLoggedOut" xml:space="preserve">
|
<data name="AccountLoggedOut" xml:space="preserve">
|
||||||
<value>Logged Out</value>
|
<value>Uitgeteken</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountSwitchedAutomatically" xml:space="preserve">
|
<data name="AccountSwitchedAutomatically" xml:space="preserve">
|
||||||
<value>Switched to next available account</value>
|
<value>Oorgeskakel na volgende beskikbare rekening</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountLockedSuccessfully" xml:space="preserve">
|
<data name="AccountLockedSuccessfully" xml:space="preserve">
|
||||||
<value>Account Locked</value>
|
<value>Rekening is vergrendel</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
|
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
|
||||||
<value>Account logged out successfully</value>
|
<value>Rekening suksesvol uitgeteken</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountRemovedSuccessfully" xml:space="preserve">
|
<data name="AccountRemovedSuccessfully" xml:space="preserve">
|
||||||
<value>Account removed successfully</value>
|
<value>Rekening suksesvol verwyder</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeleteAccount" xml:space="preserve">
|
<data name="DeleteAccount" xml:space="preserve">
|
||||||
<value>Skrap rekening</value>
|
<value>Skrap rekening</value>
|
||||||
@@ -2147,7 +2184,7 @@
|
|||||||
<value>Ongeldige bevestigingskode.</value>
|
<value>Ongeldige bevestigingskode.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RequestOTP" xml:space="preserve">
|
<data name="RequestOTP" xml:space="preserve">
|
||||||
<value>Request one-time password</value>
|
<value>Versoek eenmalige wagwoord</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SendCode" xml:space="preserve">
|
<data name="SendCode" xml:space="preserve">
|
||||||
<value>Verstuur kode</value>
|
<value>Verstuur kode</value>
|
||||||
@@ -2156,24 +2193,124 @@
|
|||||||
<value>Verstuur</value>
|
<value>Verstuur</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CopySendLinkOnSave" xml:space="preserve">
|
<data name="CopySendLinkOnSave" xml:space="preserve">
|
||||||
<value>Copy Send link on save</value>
|
<value>Kopieer Send-skakel by bewaar</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SendingCode" xml:space="preserve">
|
<data name="SendingCode" xml:space="preserve">
|
||||||
<value>Sending code</value>
|
<value>Verstuur tans kode</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Verifying" xml:space="preserve">
|
<data name="Verifying" xml:space="preserve">
|
||||||
<value>Verifying</value>
|
<value>Bevestiging</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ResendCode" xml:space="preserve">
|
<data name="ResendCode" xml:space="preserve">
|
||||||
<value>Resend Code</value>
|
<value>Stuur kode weer</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AVerificationCodeWasSentToYourEmail" xml:space="preserve">
|
<data name="AVerificationCodeWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>A verification code was sent to your email</value>
|
<value>’n Bevestigingskakel is na u e-pos gestuur</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain" xml:space="preserve">
|
<data name="AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain" xml:space="preserve">
|
||||||
<value>An error occurred while sending a verification code to your email. Please try again</value>
|
<value>’n fout het voorgekom toe ’n bevestigingskakel na u e-pos gestuur is. Probeer asb. weer</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Enter the verification code that was sent to your email</value>
|
<value>Voer die bevestigingskakel wat na u e-pos gestuur is, in</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Dien uitbomverslae in</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Help Bitwarden om toepstabiliteit te verbeter deur uitbomverslae in te dien.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Opsies is uitgevou, tik om in te vou.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Opsies is ingevou, tik om uit te vou.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Hoofletters (A tot Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Kleinletters (A tot Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Syfers (0 tot 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Spesiale karakters (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tik om terug te gaan</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Wagwoord is sigbaar, tik om te verberg.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Wagwoord is nie sigbaar, tik om te toon.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filter items per kluis</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>Alle kluise</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Kluise</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Kluis: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>Alle</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Verification Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium subscription required</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Cannot add authenticator key? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Scan QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Cannot scan QR Code? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Authenticator Key</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Enter Key Manually</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Deur u vergrendelopsies na “Nooit” te stel, is u kluis beskikbaar aan enigeen met toegang tot u toestel. Indien u hierdie opsie gebruik moet u seker maak dat u u toestel voldoende beskerm.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>Een of meer ingevoerde bronadresse is ongeldig. Hersien dit asb. en probeer weer bewaar.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>Ons kon nie u versoek verwerk nie. Probeer asb. weer of kontak ons.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Laat skermopname toe</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Is u seker u wil skermopname aktiveer?</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
2317
src/App/Resources/AppResources.ar.resx
Normal file
2317
src/App/Resources/AppResources.ar.resx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -299,6 +299,10 @@
|
|||||||
<value>Anbarım</value>
|
<value>Anbarım</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Kimlik təsdiqləyici</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Ad</value>
|
<value>Ad</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Fəallaşdırıldı</value>
|
<value>Fəallaşdırıldı</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Bağlı</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>Açıq</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Vəziyyət</value>
|
<value>Vəziyyət</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Kimlik təsdiqləyici açarı oxuna bilmir.</value>
|
<value>Kimlik təsdiqləyici açarı oxuna bilmir.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Skan avtomatik olaraq icra ediləcək.</value>
|
<value>Kameranızı QR koduna yönəldin.
|
||||||
</data>
|
Skan prosesi avtomatik baş tutacaq.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Kameranı QR koduna yönləndirin.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>QR kodu skan edin</value>
|
<value>QR kodu skan edin</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>TOTP-ni kopyala</value>
|
<value>TOTP-ni kopyala</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Hesabınıza əlavə edilən kimlik təsdiqləyici açarı varsa, giriş məlumatları avto-doldurulanda TOTP təsdiqləmə kodu da avtomatik olaraq lövhəyə kopyalanacaq.</value>
|
<value>Bir girişin, kimlik təsdiqləyici açarı varsa, giriş məlumatları avto-doldurulanda TOTP təsdiqləmə kodunu kopyalayın.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Avtomatik TOTP kopyalamasını sıradan çıxart</value>
|
<value>TOTP-ni avtomatik kopyala</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Bu özəlliyi istifadə etmək üçün premium üzvlük lazımdır.</value>
|
<value>Bu özəlliyi istifadə etmək üçün premium üzvlük lazımdır.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Bitmə vaxtı</value>
|
<value>Bitmə vaxtı</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Veb sayt nişanlarını sıradan çıxart</value>
|
<value>Veb sayt nişanlarını göstər</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Veb sayt nişanları, anbarınızda hər bir giriş elementinin yanında tanımağınıza kömək edən bir təsvir təqdim edir.</value>
|
<value>Hər girişin yanında tanına bilən təsvir göstər.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>Nişan server URL-si</value>
|
<value>Nişan server URL-si</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Kolleksiya axtar</value>
|
<value>Kolleksiya axtar</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Qovluq axtar</value>
|
<value>Fayl "Send"lərini axtar</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Axtarış növü</value>
|
<value>Mətn "Send"lərini axtar</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Axtar: {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Növ</value>
|
<value>Növ</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>İlkin (Sistem)</value>
|
<value>İlkin (Sistem)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>İlkin tünd tema</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Cihazınızda tünd rejim fəal olanda İlkin (Sistem) temanı istifadə edərkən istifadə ediləcək tünd temanı seçin</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Qeydləri kopyala</value>
|
<value>Qeydləri kopyala</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Qara</value>
|
<value>Qara</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Qara siyahıdakı URI-lər</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Qara siyahıdakı URI-lərə avto-doldurma təklif edilməyəcək. Siyahı vergüllə ayrılmalıdır. Məs. "https://twitter.com, androidapp://com.twitter.android".</value>
|
<value>Əngəllənən URI-lərin avto-doldurulması</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Saxlama istəyini sıradan çıxart</value>
|
<value>Əngəllənən URI-lər üçün avto-doldurma təklif edilmir. Çoxlu URI-ni vergüllə ayırır. Nümunə: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>"Saxlama istəyi", ilk dəfə istifadə etdiyiniz məlumatları anbarda saxlamaq istəyib-istəmədiyinizi avtomatik olaraq soruşur.</value>
|
<value>Giriş əlavə etmək üçün soruş</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Anbarınızda yoxdursa, bir element əlavə etməyi soruşun.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Tətbiq yenidən başladılanda</value>
|
<value>Tətbiq yenidən başladılanda</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Bu "Send"i açıqlayan bir ad.</value>
|
<value>Bu "Send"i açıqlayan bir ad.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Mətn</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Mətn</value>
|
<value>Mətn</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Göndərmək istədiyiniz fayl.</value>
|
<value>Göndərmək istədiyiniz fayl.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Fayl növü seçildi.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Fayl növü seçilmədi, seçmək üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Mətn növü seçildi.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Mətn növü seçilmədi, seçmək üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Silinmə tarixi</value>
|
<value>Silinmə tarixi</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2176,4 +2213,103 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>E-poçtunuza göndərilmiş təsdiqləmə kodunu daxil edin</value>
|
<value>E-poçtunuza göndərilmiş təsdiqləmə kodunu daxil edin</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Çökmə jurnallarını göndər</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Çökmə hesabatlarını göndərərək Bitwarden-in tətbiq stabilliyini yaxşılaşdırmasına kömək edin.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Seçimlər genişləndirildi, yığcamlaşdırmaq üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Seçimlər yığcamlaşdırıldı, genişləndirmək üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Böyük hərf (A-Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Kiçik hərf (Z-A)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Rəqəmlər (0-9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Xüsusi simvollar (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Geriyə getmək üçün toxun</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Parol görünür, gizlətmək üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Parol görünmür, göstərmək üçün toxunun.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Elementləri anbara görə filtrlə</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>Bütün anbarlar</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Anbarlar</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Anbar: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>Hamısı</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Təsdiqləmə kodları</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium abunəlik tələb olunur</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Kimlik təsdiqləyici açarı oxuna bilmir? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>QR kodu skan edin</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>QR kodunu skan edə bilmədiniz? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Kimlik təsdiqləyici açarı</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Kodu əllə daxil et</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>TOTP əlavə et</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>TOTP quraşdır</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Açar uğurla daxil edildikdən sonra, açarı güvənli şəkildə saxlamaq üçün "TOTP əlavə et"i seçin</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Kilid seçimlərini "Heç vaxt" olaraq tənzimləmək, anbarınızı cihazınıza müraciəti olan hər kəsə əlçatan edir. Bu seçimi istifadə etsəniz, cihazınızı düzgün qoruduğunuza əmin olmalısınız.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>Daxil edilən bir və ya daha çox URL yararsızdır. Zəhmət olmasa nəzər salın və yenidən saxlamağa çalışın.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>Tələbinizi emal edə bilmədik. Zəhmət olmasa yenidən sınayın və ya bizimlə əlaqə saxlayın.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Ekranı çəkməyə icazə ver</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Ekranın çəkilməsini fəallaşdırmaq istədiyinizə əminsiniz?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -276,16 +276,16 @@
|
|||||||
<value>Вы ўпэўнены, што хочаце выйсці?</value>
|
<value>Вы ўпэўнены, што хочаце выйсці?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveAccount" xml:space="preserve">
|
<data name="RemoveAccount" xml:space="preserve">
|
||||||
<value>Remove Account</value>
|
<value>Выдаліць уліковы запіс</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RemoveAccountConfirmation" xml:space="preserve">
|
<data name="RemoveAccountConfirmation" xml:space="preserve">
|
||||||
<value>Are you sure you want to remove this account?</value>
|
<value>Вы ўпэўнены, што хочаце выдаліць уліковы запіс?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountAlreadyAdded" xml:space="preserve">
|
<data name="AccountAlreadyAdded" xml:space="preserve">
|
||||||
<value>Account Already Added</value>
|
<value>Уліковы запіс ужо дададзены</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
|
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
|
||||||
<value>Would you like to switch to it now?</value>
|
<value>Хочаце пераключыцца на яго зараз?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="MasterPassword" xml:space="preserve">
|
<data name="MasterPassword" xml:space="preserve">
|
||||||
<value>Асноўны пароль</value>
|
<value>Асноўны пароль</value>
|
||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>Маё сховішча</value>
|
<value>Маё сховішча</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Назва</value>
|
<value>Назва</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Уключана</value>
|
<value>Уключана</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Off</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>On</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Стан</value>
|
<value>Стан</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -798,7 +808,7 @@
|
|||||||
<value>Вы шукаеце элемент аўтазапаўнення для "{0}".</value>
|
<value>Вы шукаеце элемент аўтазапаўнення для "{0}".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LearnOrg" xml:space="preserve">
|
<data name="LearnOrg" xml:space="preserve">
|
||||||
<value>Learn About Organizations</value>
|
<value>Learn about organizations</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CannotOpenApp" xml:space="preserve">
|
<data name="CannotOpenApp" xml:space="preserve">
|
||||||
<value>Немагчыма адкрыць праграму "{0}".</value>
|
<value>Немагчыма адкрыць праграму "{0}".</value>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Немагчыма прачытаць ключ праверкі аутэнтычнасці.</value>
|
<value>Немагчыма прачытаць ключ праверкі аутэнтычнасці.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Сканіраванне будзе адбывацца аўтаматычна.</value>
|
<value>Point your camera at the QR Code.
|
||||||
</data>
|
Scanning will happen automatically.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Навядзіце камеру на QR-код.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Сканаваць QR -код</value>
|
<value>Сканаваць QR -код</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Капіяваць TOTP</value>
|
<value>Капіяваць TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Калі да вашых уліковых даных прымацаваны ключ праверкі сапраўднасці, то код пацвярджэння TOTP будзе скапіяваны пры аўтазапаўненні ўліковых даных.</value>
|
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Адключыць аўтаматычнае капіяванне TOTP</value>
|
<value>Copy TOTP automatically</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Для выкарыстання гэтай функцыі патрабуецца прэміяльны статус.</value>
|
<value>Для выкарыстання гэтай функцыі патрабуецца прэміяльны статус.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Тэрмін дзеяння</value>
|
<value>Тэрмін дзеяння</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Адключыць значкі вэб-сайтаў</value>
|
<value>Show website icons</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Значкі вэб-сайтаў паказваюцца з кожным элементам у вашым сховішчы.</value>
|
<value>Show a recognizable image next to each login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>URL-адрас сервера значкоў</value>
|
<value>URL-адрас сервера значкоў</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Пошук у калекцыі</value>
|
<value>Пошук у калекцыі</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Пошук у папцы</value>
|
<value>Search File Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Пошук па тыпу</value>
|
<value>Search Text Sends</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Search {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Тып</value>
|
<value>Тып</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Default (System)</value>
|
<value>Default (System)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Default dark theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Капіяваць нататк</value>
|
<value>Капіяваць нататк</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Чорная</value>
|
<value>Чорная</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>URI у чорным спісе</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Аўтазапаўненне не будзе прапаноўвацца для URI з чорнага спіса. Элементы гэтага спіса трэба падзяляць коскай. Напрыклад: "https://twitter.com, androidapp://com.twitter.android".</value>
|
<value>Auto-fill blocked URIs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Выключыць запыт на захаванне.</value>
|
<value>Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>Апавяшчэнне аб захаванні аўтаматычна прапануе вам захаваць новыя элементы ў сховішчы пасля іх дадання.</value>
|
<value>Ask to add login</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Ask to add an item if one isn't found in your vault.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Пры перазапуску</value>
|
<value>Пры перазапуску</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Зразумелая назва для апісання адпраўлення</value>
|
<value>Зразумелая назва для апісання адпраўлення</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Тэкст</value>
|
<value>Тэкст</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Файл, які вы хочаце адправіць.</value>
|
<value>Файл, які вы хочаце адправіць.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>File type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>File type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Text type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Text type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Дата выдалення</value>
|
<value>Дата выдалення</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2129,13 +2166,13 @@
|
|||||||
<value>Account removed successfully</value>
|
<value>Account removed successfully</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeleteAccount" xml:space="preserve">
|
<data name="DeleteAccount" xml:space="preserve">
|
||||||
<value>Delete Account</value>
|
<value>Delete account</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
|
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
|
||||||
<value>Deleting your account is permanent</value>
|
<value>Deleting your account is permanent</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeleteAccountExplanation" xml:space="preserve">
|
<data name="DeleteAccountExplanation" xml:space="preserve">
|
||||||
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
<value>Your account and all vault data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeletingYourAccount" xml:space="preserve">
|
<data name="DeletingYourAccount" xml:space="preserve">
|
||||||
<value>Deleting your account</value>
|
<value>Deleting your account</value>
|
||||||
@@ -2176,4 +2213,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Enter the verification code that was sent to your email</value>
|
<value>Enter the verification code that was sent to your email</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Submit crash logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Options are expanded, tap to collapse.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Options are collapsed, tap to expand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Uppercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Lowercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Numbers (0 to 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Special Characters (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tap to go back</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Password is visible, tap to hide.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Password is not visible, tap to show.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filter items by vault</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>All Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Vault: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>All</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Verification Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium subscription required</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Cannot add authenticator key? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Scan QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Cannot scan QR Code? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Authenticator Key</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Enter Key Manually</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>We were unable to process your request. Please try again or contact us.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Allow screen capture</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to enable Screen Capture?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -261,11 +261,11 @@
|
|||||||
<comment>The button text that allows user to launch the website to their web browser.</comment>
|
<comment>The button text that allows user to launch the website to their web browser.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="LogIn" xml:space="preserve">
|
<data name="LogIn" xml:space="preserve">
|
||||||
<value>Вписване</value>
|
<value>Вход</value>
|
||||||
<comment>The login button text (verb).</comment>
|
<comment>The login button text (verb).</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="LogInNoun" xml:space="preserve">
|
<data name="LogInNoun" xml:space="preserve">
|
||||||
<value>Вписване</value>
|
<value>Вход</value>
|
||||||
<comment>Title for login page. (noun)</comment>
|
<comment>Title for login page. (noun)</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="LogOut" xml:space="preserve">
|
<data name="LogOut" xml:space="preserve">
|
||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>Моят трезор</value>
|
<value>Моят трезор</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Име</value>
|
<value>Име</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -393,7 +397,7 @@
|
|||||||
<value>Посетете нашия сайт</value>
|
<value>Посетете нашия сайт</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||||
<value>Посетете сайта ни за помощ, новини, начини да се свържете с нас и как да ползвате Битуорден.</value>
|
<value>Посетете сайта ни за помощ, новини, начини да се свържете с нас и как да ползвате Bitwarden.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Website" xml:space="preserve">
|
<data name="Website" xml:space="preserve">
|
||||||
<value>Сайт</value>
|
<value>Сайт</value>
|
||||||
@@ -415,7 +419,7 @@
|
|||||||
<value>Разширение</value>
|
<value>Разширение</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillAccessibilityDescription" xml:space="preserve">
|
<data name="AutofillAccessibilityDescription" xml:space="preserve">
|
||||||
<value>Автоматично дописване на полетата във формулярите в приложенията и уеб чрез услугата за достъпност на Битуорден.</value>
|
<value>Автоматично дописване на полетата във формулярите в приложенията и уеб чрез услугата за достъпност на Bitwarden.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillService" xml:space="preserve">
|
<data name="AutofillService" xml:space="preserve">
|
||||||
<value>Услуга за автоматично дописване</value>
|
<value>Услуга за автоматично дописване</value>
|
||||||
@@ -424,7 +428,7 @@
|
|||||||
<value>Без нееднозначни знаци</value>
|
<value>Без нееднозначни знаци</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="BitwardenAppExtension" xml:space="preserve">
|
<data name="BitwardenAppExtension" xml:space="preserve">
|
||||||
<value>Разширение за програмите на Битуорден</value>
|
<value>Разширение за програмите на Bitwarden</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="BitwardenAppExtensionAlert2" xml:space="preserve">
|
<data name="BitwardenAppExtensionAlert2" xml:space="preserve">
|
||||||
<value>Най-лесният начин да добавяте нови записи в трезора е чрез разширението за програмите на Битуорден. Може да научите повече за него в екрана за настройките.</value>
|
<value>Най-лесният начин да добавяте нови записи в трезора е чрез разширението за програмите на Битуорден. Може да научите повече за него в екрана за настройките.</value>
|
||||||
@@ -439,7 +443,7 @@
|
|||||||
<value>Автоматично дописване на формулярите за вписване с услугата за достъпност на Битуорден.</value>
|
<value>Автоматично дописване на формулярите за вписване с услугата за достъпност на Битуорден.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChangeEmail" xml:space="preserve">
|
<data name="ChangeEmail" xml:space="preserve">
|
||||||
<value>Промяна на адреса за е-поща</value>
|
<value>Промяна на имейла</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChangeEmailConfirmation" xml:space="preserve">
|
<data name="ChangeEmailConfirmation" xml:space="preserve">
|
||||||
<value>Адресът ви за е-поща може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?</value>
|
<value>Адресът ви за е-поща може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?</value>
|
||||||
@@ -457,7 +461,7 @@
|
|||||||
<value>Продължаване</value>
|
<value>Продължаване</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CreateAccount" xml:space="preserve">
|
<data name="CreateAccount" xml:space="preserve">
|
||||||
<value>Създаване на абонамент</value>
|
<value>Регистрация</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CreatingAccount" xml:space="preserve">
|
<data name="CreatingAccount" xml:space="preserve">
|
||||||
<value>Създаване на сметката...</value>
|
<value>Създаване на сметката...</value>
|
||||||
@@ -510,7 +514,7 @@
|
|||||||
<value>Отпечатък</value>
|
<value>Отпечатък</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="GeneratePassword" xml:space="preserve">
|
<data name="GeneratePassword" xml:space="preserve">
|
||||||
<value>Нова парола</value>
|
<value>Генериране на парола</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="GetPasswordHint" xml:space="preserve">
|
<data name="GetPasswordHint" xml:space="preserve">
|
||||||
<value>Получаване на подсказка за главната парола</value>
|
<value>Получаване на подсказка за главната парола</value>
|
||||||
@@ -562,7 +566,7 @@
|
|||||||
<comment>Message shown when interacting with the server</comment>
|
<comment>Message shown when interacting with the server</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="LoginOrCreateNewAccount" xml:space="preserve">
|
<data name="LoginOrCreateNewAccount" xml:space="preserve">
|
||||||
<value>Впишете се или създайте нов абонамент, за да достъпите защитен трезор.</value>
|
<value>Влезте или се регистрирайте, за да имате достъп до вашия защитен трезор.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Manage" xml:space="preserve">
|
<data name="Manage" xml:space="preserve">
|
||||||
<value>Управление</value>
|
<value>Управление</value>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Включено</value>
|
<value>Включено</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Изключено</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>Включено</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Състояние</value>
|
<value>Състояние</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Удостоверителният ключ не може да се прочете.</value>
|
<value>Удостоверителният ключ не може да се прочете.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Сканирането става автоматично.</value>
|
<value>Насочете камерата към QR кода.
|
||||||
</data>
|
Сканирането ще се извърши автоматично.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Насочете камерата към QR кода.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Сканиране на QR код</value>
|
<value>Сканиране на QR код</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Копиране на кода за потвърждаване</value>
|
<value>Копиране на кода за потвърждаване</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Ако към вписването има и удостоверителен ключ, кодът за потвърждаване се копира автоматично в буфера при автоматично дописване.</value>
|
<value>Ако към вписването има и удостоверителен ключ, кодът за потвърждаване се копира автоматично в буфера при автоматично дописване.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Без автоматично копиране на временни, еднократни пароли</value>
|
<value>Автоматично копиране на TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>За да се възползвате от тази възможност, трябва да ползвате платен абонамент.</value>
|
<value>За да се възползвате от тази възможност, трябва да ползвате платен абонамент.</value>
|
||||||
@@ -1057,7 +1065,7 @@
|
|||||||
<value>февруари</value>
|
<value>февруари</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FirstName" xml:space="preserve">
|
<data name="FirstName" xml:space="preserve">
|
||||||
<value>Собствено име</value>
|
<value>Име</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="January" xml:space="preserve">
|
<data name="January" xml:space="preserve">
|
||||||
<value>януари</value>
|
<value>януари</value>
|
||||||
@@ -1069,7 +1077,7 @@
|
|||||||
<value>юни</value>
|
<value>юни</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LastName" xml:space="preserve">
|
<data name="LastName" xml:space="preserve">
|
||||||
<value>Фамилно име</value>
|
<value>Фамилия</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FullName" xml:space="preserve">
|
<data name="FullName" xml:space="preserve">
|
||||||
<value>Пълно име</value>
|
<value>Пълно име</value>
|
||||||
@@ -1128,17 +1136,17 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Изтичане</value>
|
<value>Изтичане</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Изключване на иконките на сайтовете</value>
|
<value>Показване на иконките на уеб сайтовете</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Иконките на сайтовете са разпознаваемо изображение за всеки запис в трезора.</value>
|
<value>Показване на разпознаваемо изображение до всеки запис.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>Адрес на сървъра с иконки</value>
|
<value>Адрес на сървъра с иконки</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillWithBitwarden" xml:space="preserve">
|
<data name="AutofillWithBitwarden" xml:space="preserve">
|
||||||
<value>Автоматично дописване с Битуорден</value>
|
<value>Автоматично дописване с Bitwarden</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="VaultIsLocked" xml:space="preserve">
|
<data name="VaultIsLocked" xml:space="preserve">
|
||||||
<value>Трезорът е заключен</value>
|
<value>Трезорът е заключен</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Търсене в колекцията</value>
|
<value>Търсене в колекцията</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Търсене в папката</value>
|
<value>Търсене в изпратените файлове</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Търсене по вид</value>
|
<value>Търсене в изпратените текстове</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Търсене на {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Вид</value>
|
<value>Вид</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>По подразбиране (от системата)</value>
|
<value>По подразбиране (от системата)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Стандартен тъмен облик</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Изберете тъмния облик, който да се ползва, когато е избран стандартният (от системата) облик и тъмният режим на устройството Ви е включен.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Копиране на бележките</value>
|
<value>Копиране на бележките</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Запрет</value>
|
<value>Запрет</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Адреси под запрет</value>
|
<value>Норд</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Няма да се предлага автоматично дописване във формулярите с адреси под запрет. За разделител в списъка ползвайте запетая. Напр: „https://twitter.com, androidapp://com.twitter.android“.</value>
|
<value>Блокирани за авт. попълване адреси</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Без напомняне за запомняне</value>
|
<value>За посочените блокирани адреси няма да се прави автоматично попълване. Разделяйте отделните адреси със запетая. Пример: „https://twitter.com, androidapp://com.twitter.android“.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>Дали да бъдете автоматично подканени да запазите в трезора данни за вписване, които въвеждате за първи път.</value>
|
<value>Питане за добавяне на запис</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Питане за добавяне на запис, ако такъв не бъде намерен в трезора Ви.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>При рестартиране на приложението</value>
|
<value>При рестартиране на приложението</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Описателно име за това изпращане.</value>
|
<value>Описателно име за това изпращане.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Текст</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Текст</value>
|
<value>Текст</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Файл за изпращане.</value>
|
<value>Файл за изпращане.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Избран е файлов тип.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Не е избран файлов тип. Докоснете за избор.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Избран е текстов тип.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Не е избран текстов тип. Докоснете за избор.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Дата на изтриване</value>
|
<value>Дата на изтриване</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2177,4 +2214,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Въведете кода за потвърждаване, който беше изпратен на Вашата е-поща</value>
|
<value>Въведете кода за потвърждаване, който беше изпратен на Вашата е-поща</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Изпращане на доклади за сривовете</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Помогнете на Битуордън в подобряването на стабилността на приложението, като изпращате доклади относно сривовете.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Възможностите са разгънати. Докоснете за свиване.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Възможностите са свити. Докоснете за разгъване.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Главни букви (A-Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Малки букви (a-z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Числа (0-9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Специални знаци (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Досокнете за връщане назад</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Паролата е видима. Докоснете за скриване.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Паролата не е видима. Докоснете за показване.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Филтриране на записите по трезор</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>Всички трезори</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Трезори</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Трезор: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>Всички</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Кодове за потвърждаване</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Изисква се платен абонамент</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Не можете да добавите удостоверителен ключ? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Сканиране на QR код</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Не можете да сканирате QR кода? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Ключ за удостоверяване</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Ръчно въвеждане на кода</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Ако изберете „Никога“ като настройка за заключването, трезорът Ви ще бъде достъпен за всеки, който има досег с устройството. Ако използвате тази настройка, трябва да се уверите, че устройството Ви е удачно защитено.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>Един или повече от въведените адреси са неправилни. Прегледайте ги и ги поправете, а след това опитайте да запазите отново.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>Не може да обработим заявката ви. Моля опитайте отново или се свържете с нас.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Позволяване на заснемане на екрана</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Наистина ли искате да разрешите заснемането на екрана?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>আমার ভল্ট</value>
|
<value>আমার ভল্ট</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>নাম</value>
|
<value>নাম</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -412,13 +416,13 @@
|
|||||||
<value>Add an Item</value>
|
<value>Add an Item</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AppExtension" xml:space="preserve">
|
<data name="AppExtension" xml:space="preserve">
|
||||||
<value>App Extension</value>
|
<value>App extension</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillAccessibilityDescription" xml:space="preserve">
|
<data name="AutofillAccessibilityDescription" xml:space="preserve">
|
||||||
<value>Use the Bitwarden accessibility service to auto-fill your logins across apps and the web.</value>
|
<value>Use the Bitwarden accessibility service to auto-fill your logins across apps and the web.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillService" xml:space="preserve">
|
<data name="AutofillService" xml:space="preserve">
|
||||||
<value>Auto-fill Service</value>
|
<value>Auto-fill service</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AvoidAmbiguousCharacters" xml:space="preserve">
|
<data name="AvoidAmbiguousCharacters" xml:space="preserve">
|
||||||
<value>Avoid Ambiguous Characters</value>
|
<value>Avoid Ambiguous Characters</value>
|
||||||
@@ -473,13 +477,13 @@
|
|||||||
<value>আপনার প্রধান পাসওয়ার্ডের ইঙ্গিতটি পেতে আপনার অ্যাকাউন্টের ইমেল ঠিকানা প্রবেশ করুন।</value>
|
<value>আপনার প্রধান পাসওয়ার্ডের ইঙ্গিতটি পেতে আপনার অ্যাকাউন্টের ইমেল ঠিকানা প্রবেশ করুন।</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExntesionReenable" xml:space="preserve">
|
<data name="ExntesionReenable" xml:space="preserve">
|
||||||
<value>Re-enable App Extension</value>
|
<value>Re-enable app extension</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExtensionAlmostDone" xml:space="preserve">
|
<data name="ExtensionAlmostDone" xml:space="preserve">
|
||||||
<value>Almost done!</value>
|
<value>Almost done!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExtensionEnable" xml:space="preserve">
|
<data name="ExtensionEnable" xml:space="preserve">
|
||||||
<value>Enable App Extension</value>
|
<value>Enable app extension</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExtensionInSafari" xml:space="preserve">
|
<data name="ExtensionInSafari" xml:space="preserve">
|
||||||
<value>In Safari, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu).</value>
|
<value>In Safari, find Bitwarden using the share icon (hint: scroll to the right on the bottom row of the menu).</value>
|
||||||
@@ -647,7 +651,7 @@
|
|||||||
<comment>Push notifications for apple products</comment>
|
<comment>Push notifications for apple products</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="RateTheApp" xml:space="preserve">
|
<data name="RateTheApp" xml:space="preserve">
|
||||||
<value>Rate the App</value>
|
<value>Rate the app</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="RateTheAppDescription" xml:space="preserve">
|
<data name="RateTheAppDescription" xml:space="preserve">
|
||||||
<value>দয়া করে একটি ভাল পর্যালোচনার মাধ্যমে সাহায্য করতে আমাদের বিবেচনা করুন!</value>
|
<value>দয়া করে একটি ভাল পর্যালোচনার মাধ্যমে সাহায্য করতে আমাদের বিবেচনা করুন!</value>
|
||||||
@@ -694,14 +698,14 @@
|
|||||||
<value>Syncing failed.</value>
|
<value>Syncing failed.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SyncVaultNow" xml:space="preserve">
|
<data name="SyncVaultNow" xml:space="preserve">
|
||||||
<value>Sync Vault Now</value>
|
<value>Sync vault now</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TouchID" xml:space="preserve">
|
<data name="TouchID" xml:space="preserve">
|
||||||
<value>Touch ID</value>
|
<value>Touch ID</value>
|
||||||
<comment>What Apple calls their fingerprint reader.</comment>
|
<comment>What Apple calls their fingerprint reader.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="TwoStepLogin" xml:space="preserve">
|
<data name="TwoStepLogin" xml:space="preserve">
|
||||||
<value>Two-step Login</value>
|
<value>Two-step login</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||||
<value>Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
<value>Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||||
@@ -710,7 +714,7 @@
|
|||||||
<value>Unlock with {0}</value>
|
<value>Unlock with {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="UnlockWithPIN" xml:space="preserve">
|
<data name="UnlockWithPIN" xml:space="preserve">
|
||||||
<value>Unlock with PIN Code</value>
|
<value>Unlock with PIN code</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Validating" xml:space="preserve">
|
<data name="Validating" xml:space="preserve">
|
||||||
<value>Validating</value>
|
<value>Validating</value>
|
||||||
@@ -723,7 +727,7 @@
|
|||||||
<value>View Item</value>
|
<value>View Item</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="WebVault" xml:space="preserve">
|
<data name="WebVault" xml:space="preserve">
|
||||||
<value>Bitwarden Web Vault</value>
|
<value>Bitwarden web vault</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Lost2FAApp" xml:space="preserve">
|
<data name="Lost2FAApp" xml:space="preserve">
|
||||||
<value>Lost authenticator app?</value>
|
<value>Lost authenticator app?</value>
|
||||||
@@ -733,7 +737,7 @@
|
|||||||
<comment>Screen title</comment>
|
<comment>Screen title</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExtensionActivated" xml:space="preserve">
|
<data name="ExtensionActivated" xml:space="preserve">
|
||||||
<value>Extension Activated!</value>
|
<value>Extension activated!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Icons" xml:space="preserve">
|
<data name="Icons" xml:space="preserve">
|
||||||
<value>Icons</value>
|
<value>Icons</value>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Enabled</value>
|
<value>Enabled</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Off</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>On</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -798,7 +808,7 @@
|
|||||||
<value>You are searching for an auto-fill item for "{0}".</value>
|
<value>You are searching for an auto-fill item for "{0}".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LearnOrg" xml:space="preserve">
|
<data name="LearnOrg" xml:space="preserve">
|
||||||
<value>Learn About Organizations</value>
|
<value>Learn about organizations</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CannotOpenApp" xml:space="preserve">
|
<data name="CannotOpenApp" xml:space="preserve">
|
||||||
<value>Cannot open the app "{0}".</value>
|
<value>Cannot open the app "{0}".</value>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Cannot read authenticator key.</value>
|
<value>Cannot read authenticator key.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Scanning will happen automatically.</value>
|
<value>Point your camera at the QR Code.
|
||||||
</data>
|
Scanning will happen automatically.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Point your camera at the QR code.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Scan QR Code</value>
|
<value>Scan QR Code</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Copy TOTP</value>
|
<value>Copy TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login.</value>
|
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Disable Automatic TOTP Copy</value>
|
<value>Copy TOTP automatically</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>A premium membership is required to use this feature.</value>
|
<value>A premium membership is required to use this feature.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Expiration</value>
|
<value>Expiration</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Disable Website Icons</value>
|
<value>Show website icons</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Website Icons provide a recognizable image next to each login item in your vault.</value>
|
<value>Show a recognizable image next to each login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>Icons Server URL</value>
|
<value>Icons Server URL</value>
|
||||||
@@ -1311,7 +1319,7 @@
|
|||||||
<value>5. Select "Bitwarden"</value>
|
<value>5. Select "Bitwarden"</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PasswordAutofill" xml:space="preserve">
|
<data name="PasswordAutofill" xml:space="preserve">
|
||||||
<value>Password AutoFill</value>
|
<value>Password auto-fill</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
||||||
<value>The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen.</value>
|
<value>The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen.</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>সংগ্রহ অনুসন্ধান</value>
|
<value>সংগ্রহ অনুসন্ধান</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>ফোল্ডার অনুসন্ধান</value>
|
<value>Search File Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>অনুসন্ধানের ধরন</value>
|
<value>Search Text Sends</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Search {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>ধরন</value>
|
<value>ধরন</value>
|
||||||
@@ -1445,7 +1457,7 @@
|
|||||||
<value>There are no folders to list.</value>
|
<value>There are no folders to list.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="FingerprintPhrase" xml:space="preserve">
|
<data name="FingerprintPhrase" xml:space="preserve">
|
||||||
<value>Fingerprint Phrase</value>
|
<value>Fingerprint phrase</value>
|
||||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="YourAccountsFingerprint" xml:space="preserve">
|
<data name="YourAccountsFingerprint" xml:space="preserve">
|
||||||
@@ -1456,10 +1468,10 @@
|
|||||||
<value>Bitwarden allows you to share your vault items with others by using an organization account. Would you like to visit the bitwarden.com website to learn more?</value>
|
<value>Bitwarden allows you to share your vault items with others by using an organization account. Would you like to visit the bitwarden.com website to learn more?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ExportVault" xml:space="preserve">
|
<data name="ExportVault" xml:space="preserve">
|
||||||
<value>Export Vault</value>
|
<value>Export vault</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LockNow" xml:space="preserve">
|
<data name="LockNow" xml:space="preserve">
|
||||||
<value>Lock Now</value>
|
<value>Lock now</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PIN" xml:space="preserve">
|
<data name="PIN" xml:space="preserve">
|
||||||
<value>PIN</value>
|
<value>PIN</value>
|
||||||
@@ -1537,8 +1549,14 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Default (System)</value>
|
<value>Default (System)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Default dark theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Copy Notes</value>
|
<value>Copy Note</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Exit" xml:space="preserve">
|
<data name="Exit" xml:space="preserve">
|
||||||
<value>প্রস্থান</value>
|
<value>প্রস্থান</value>
|
||||||
@@ -1553,20 +1571,24 @@
|
|||||||
<value>কালো</value>
|
<value>কালো</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Blacklisted URIs</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>URIs that are blacklisted will not offer auto-fill. The list should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android".</value>
|
<value>Auto-fill blocked URIs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Disable Save Prompt</value>
|
<value>Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>The "Save Prompt" automatically prompts you to save new items to your vault whenever you enter them for the first time.</value>
|
<value>Ask to add login</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Ask to add an item if one isn't found in your vault.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>On App Restart</value>
|
<value>On app restart</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||||
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
||||||
@@ -1800,16 +1822,16 @@
|
|||||||
<value>Bitwarden needs attention - Enable "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
<value>Bitwarden needs attention - Enable "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillServices" xml:space="preserve">
|
<data name="AutofillServices" xml:space="preserve">
|
||||||
<value>Auto-fill Services</value>
|
<value>Auto-fill services</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InlineAutofill" xml:space="preserve">
|
<data name="InlineAutofill" xml:space="preserve">
|
||||||
<value>Use Inline Autofill</value>
|
<value>Use inline autofill</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InlineAutofillDescription" xml:space="preserve">
|
<data name="InlineAutofillDescription" xml:space="preserve">
|
||||||
<value>Use inline autofill if your selected IME (keyboard) supports it. If your configuration is not supported (or this option is disabled), the default Autofill overlay will be used.</value>
|
<value>Use inline autofill if your selected IME (keyboard) supports it. If your configuration is not supported (or this option is disabled), the default Autofill overlay will be used.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Accessibility" xml:space="preserve">
|
<data name="Accessibility" xml:space="preserve">
|
||||||
<value>Use Accessibility</value>
|
<value>Use accessibility</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccessibilityDescription" xml:space="preserve">
|
<data name="AccessibilityDescription" xml:space="preserve">
|
||||||
<value>Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. When enabled, we'll display a popup when login fields are selected.</value>
|
<value>Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. When enabled, we'll display a popup when login fields are selected.</value>
|
||||||
@@ -1824,7 +1846,7 @@
|
|||||||
<value>Required to use the Autofill Quick-Action Tile, or to augment the Autofill Service by using Draw-Over (if enabled).</value>
|
<value>Required to use the Autofill Quick-Action Tile, or to augment the Autofill Service by using Draw-Over (if enabled).</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DrawOver" xml:space="preserve">
|
<data name="DrawOver" xml:space="preserve">
|
||||||
<value>Use Draw-Over</value>
|
<value>Use draw-over</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DrawOverDescription" xml:space="preserve">
|
<data name="DrawOverDescription" xml:space="preserve">
|
||||||
<value>When enabled, allows the Bitwarden Accessibility Service to display a popup when login fields are selected.</value>
|
<value>When enabled, allows the Bitwarden Accessibility Service to display a popup when login fields are selected.</value>
|
||||||
@@ -1857,6 +1879,9 @@
|
|||||||
<value>A friendly name to describe this Send.</value>
|
<value>A friendly name to describe this Send.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Text</value>
|
<value>Text</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1873,6 +1898,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>The file you want to send.</value>
|
<value>The file you want to send.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>File type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>File type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Text type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Text type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Deletion Date</value>
|
<value>Deletion Date</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2130,13 +2167,13 @@
|
|||||||
<value>Account removed successfully</value>
|
<value>Account removed successfully</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeleteAccount" xml:space="preserve">
|
<data name="DeleteAccount" xml:space="preserve">
|
||||||
<value>Delete Account</value>
|
<value>Delete account</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
|
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
|
||||||
<value>Deleting your account is permanent</value>
|
<value>Deleting your account is permanent</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeleteAccountExplanation" xml:space="preserve">
|
<data name="DeleteAccountExplanation" xml:space="preserve">
|
||||||
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
<value>Your account and all vault data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DeletingYourAccount" xml:space="preserve">
|
<data name="DeletingYourAccount" xml:space="preserve">
|
||||||
<value>Deleting your account</value>
|
<value>Deleting your account</value>
|
||||||
@@ -2177,4 +2214,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Enter the verification code that was sent to your email</value>
|
<value>Enter the verification code that was sent to your email</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Submit crash logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Options are expanded, tap to collapse.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Options are collapsed, tap to expand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Uppercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Lowercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Numbers (0 to 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Special Characters (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tap to go back</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Password is visible, tap to hide.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Password is not visible, tap to show.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filter items by vault</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>All Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Vault: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>All</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Verification Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium subscription required</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Cannot add authenticator key? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Scan QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Cannot scan QR Code? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Authenticator Key</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Enter Key Manually</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>We were unable to process your request. Please try again or contact us.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Allow screen capture</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to enable Screen Capture?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>Moj trezor</value>
|
<value>Moj trezor</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Ime</value>
|
<value>Ime</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Omogućeno</value>
|
<value>Omogućeno</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Off</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>On</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Ne može se pročitati ključ autentifikatora.</value>
|
<value>Ne može se pročitati ključ autentifikatora.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Skeniranje će se odviti automatski.</value>
|
<value>Point your camera at the QR Code.
|
||||||
</data>
|
Scanning will happen automatically.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Usmerite kameru prema QR kodu.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Skenirajte QR kod</value>
|
<value>Skenirajte QR kod</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Iskopirajte TOTP</value>
|
<value>Iskopirajte TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Ako je za Vašu prijavu priložen autentifikacioni ključ, TOTP verifikacioni kod se automatski kopira u Vašu memoriju kad god automatski popunite prijavu.</value>
|
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Onemogućite automatsko TOTP kopiranje</value>
|
<value>Copy TOTP automatically</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Za korišćenje ove funkcije potrebno je premium članstvo.</value>
|
<value>Za korišćenje ove funkcije potrebno je premium članstvo.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Rok upotrebe</value>
|
<value>Rok upotrebe</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Onemogućite ikone veb lokacije</value>
|
<value>Show website icons</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Ikone veb lokacije pružaju prepoznatljivu sliku pored svake stavke za prijavu u Vaš trezor.</value>
|
<value>Show a recognizable image next to each login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>URL adresa servera ikona</value>
|
<value>URL adresa servera ikona</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Pretraži kolekciju</value>
|
<value>Pretraži kolekciju</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Pretraži folder</value>
|
<value>Pretraži datoteke za slanje</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Tip pretrage</value>
|
<value>Pretraži tekstove za slanje</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Pretraži {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Vrsta</value>
|
<value>Vrsta</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Zadano (Sistem)</value>
|
<value>Zadano (Sistem)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Zadana tamna tema</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Kopiraj bilješke</value>
|
<value>Kopiraj bilješke</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Crna</value>
|
<value>Crna</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Crna lista URI-ja</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>URI koji su na crnoj listi neće nuditi auto-ispunu. Popis treba odvojiti zarezima. Npr.: „https://webstranica.com, androidapp://com.aplikcacija.android”.</value>
|
<value>Auto-fill blocked URIs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Onemogući upit za spremanje</value>
|
<value>Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>Upit za spremanje novih stavki automatski se pojavlju nakon prepoznatog prvog unosa novih stavki.</value>
|
<value>Ask to add login</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Ask to add an item if one isn't found in your vault.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Kod ponovnog pokretanja</value>
|
<value>Kod ponovnog pokretanja</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Nadimak za ovaj Send</value>
|
<value>Nadimak za ovaj Send</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Tekst</value>
|
<value>Tekst</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Datoteka koju želiš poslati</value>
|
<value>Datoteka koju želiš poslati</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>File type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>File type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Text type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Text type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Datum brisanja</value>
|
<value>Datum brisanja</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2176,4 +2213,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Unesi verifikacijski kod koji je poslan na tvoj E-Mail</value>
|
<value>Unesi verifikacijski kod koji je poslan na tvoj E-Mail</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Submit crash logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Options are expanded, tap to collapse.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Options are collapsed, tap to expand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Uppercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Lowercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Numbers (0 to 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Special Characters (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tap to go back</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Password is visible, tap to hide.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Password is not visible, tap to show.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filter items by vault</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>All Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Vault: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>All</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Verification Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium subscription required</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Cannot add authenticator key? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Scan QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Cannot scan QR Code? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Authenticator Key</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Enter Key Manually</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>We were unable to process your request. Please try again or contact us.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Allow screen capture</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to enable Screen Capture?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>La meua caixa forta</value>
|
<value>La meua caixa forta</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Autenticador</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Nom</value>
|
<value>Nom</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Habilitat</value>
|
<value>Habilitat</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Desactivat</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>Activat</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Estat</value>
|
<value>Estat</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>No es pot llegir la clau d'autenticació.</value>
|
<value>No es pot llegir la clau d'autenticació.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>L'escaneig serà automàtic.</value>
|
<value>Apunteu la càmera cap al codi QR.
|
||||||
</data>
|
L'escaneig es farà automàticament.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Dirigiu la càmera al codi QR.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Escaneja el codi QR</value>
|
<value>Escaneja el codi QR</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Copia TOTP</value>
|
<value>Copia TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Si el vostre inici de sessió té una clau d'autenticació associada, el codi de verificació TOTP es copiarà al vostre porta-retalls quan s'òmpliga automàticament l'inici de sessió.</value>
|
<value>Si el vostre inici de sessió té una clau d'autenticació associada, el codi de verificació TOTP es copiarà al vostre porta-retalls quan s'òmpliga automàticament l'inici de sessió.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Deshabilita la còpia TOTP automàtica</value>
|
<value>Copia TOTP automaticament</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Cal una subscripció premium per utilitzar aquesta característica.</value>
|
<value>Cal una subscripció premium per utilitzar aquesta característica.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Caducitat</value>
|
<value>Caducitat</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Deshabilita icones del lloc web</value>
|
<value>Mostra les icones del lloc web</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Les icones del lloc web proporcionen una imatge que es pot reconèixer al costat de cada element d'inici de sessió a la vostra caixa forta.</value>
|
<value>Mostra una imatge reconeixible al costat de cada inici de sessió.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>URL del servidor d'icones</value>
|
<value>URL del servidor d'icones</value>
|
||||||
@@ -1299,7 +1307,7 @@
|
|||||||
<value>1. Aneu a l'aplicació "Configuració" de l'iOS</value>
|
<value>1. Aneu a l'aplicació "Configuració" de l'iOS</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillTurnOn2" xml:space="preserve">
|
<data name="AutofillTurnOn2" xml:space="preserve">
|
||||||
<value>2. Toqueu "Contrasenyes i comptes"</value>
|
<value>2. Toqueu "Contrasenyes"</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillTurnOn3" xml:space="preserve">
|
<data name="AutofillTurnOn3" xml:space="preserve">
|
||||||
<value>3. Toqueu "Emplenament automàtic de contrasenyes"</value>
|
<value>3. Toqueu "Emplenament automàtic de contrasenyes"</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Cerca a la col·lecció</value>
|
<value>Cerca a la col·lecció</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Cerca a la carpeta</value>
|
<value>Cerca fitxers Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Tipus de cerca</value>
|
<value>Cerca Sends de text</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Cerca {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Tipus</value>
|
<value>Tipus</value>
|
||||||
@@ -1513,7 +1525,7 @@
|
|||||||
<value>2 minuts</value>
|
<value>2 minuts</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ClearClipboard" xml:space="preserve">
|
<data name="ClearClipboard" xml:space="preserve">
|
||||||
<value>Neteja el porta-retalls</value>
|
<value>Buida el porta-retalls</value>
|
||||||
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="ClearClipboardDescription" xml:space="preserve">
|
<data name="ClearClipboardDescription" xml:space="preserve">
|
||||||
@@ -1537,8 +1549,14 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Per defecte (Sistema)</value>
|
<value>Per defecte (Sistema)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Tema fosc per defecte</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Tria el tema fosc quan feu servir el tema predeterminat (sistema) mentre el mode fosc del vostre dispositiu està habilitat</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Copia notes</value>
|
<value>Copia nota</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Exit" xml:space="preserve">
|
<data name="Exit" xml:space="preserve">
|
||||||
<value>Tanca</value>
|
<value>Tanca</value>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Negre</value>
|
<value>Negre</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>URl a la llista negra</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Els URI que es mostren a la llista negra no oferiran l’emplenament automàtic. La llista ha de estar separada per comes. Ex: "https://twitter.com, androidapp: //com.twitter.android".</value>
|
<value>Emplena automàticament els URI bloquejats</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Desactiva Guarda elements nous</value>
|
<value>L'emplenament automàtic no s'oferirà per als URI bloquejats. Separeu diversos URI amb una coma. Per exemple: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>"Guarda elements nous" demanarà automàticament que guardeu elements nous al magatzem cada volta que els introduïu per primera vegada.</value>
|
<value>Sol·licita afegir els inicis de sessió</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Sol·licita afegir un element si no se'n troba cap a la caixa forta.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>En reiniciar l'aplicació</value>
|
<value>En reiniciar l'aplicació</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Un nom apropiat per descriure aquest Send.</value>
|
<value>Un nom apropiat per descriure aquest Send.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Text</value>
|
<value>Text</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>El fitxer que voleu enviar.</value>
|
<value>El fitxer que voleu enviar.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>El tipus de fitxer està seleccionat.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>El tipus de fitxer no està seleccionat, toqueu per a seleccionar-lo.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>El tipus de text està seleccionat.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>El tipus de text no està seleccionat, toqueu per a seleccionar-lo.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Data de supressió</value>
|
<value>Data de supressió</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2176,4 +2213,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Introduïu el codi de verificació que s’ha enviat al vostre correu</value>
|
<value>Introduïu el codi de verificació que s’ha enviat al vostre correu</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Envieu registres d'error</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Ajudeu Bitwarden a millorar l'estabilitat de l'aplicació enviant els registres d'error</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Les opcions estan ampliades, toqueu per contraure.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Les opcions estan reduïdes, toqueu per ampliar.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Majúscules (A a la Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Minúscules (A a la Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Números (0 al 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Caràcters especials (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Toca per a tornar arrere</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>La contrasenya és visible, toqueu per amagar-la.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>La contrasenya no és visible, toqueu per mostrar-la.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filtra artícless per caixa forta</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>Totes les caixes fortes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Caixes fortes</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Caixa forta: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>Totes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Codis de verificació</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Cal una subscripció premium</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>No es pot afegir la clau d'autenticació? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Escaneja el codi QR</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>No podeu escanejar el codi QR? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Clau autenticadora</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Introdueix la clau manualment</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Afegeix TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Configura TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Una vegada introduïda la clau correctament,
|
||||||
|
seleccioneu Afegeix TOTP per emmagatzemar la clau de manera segura</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Si configureu les opcions de bloqueig a "Mai", la vostra caixa forta està disponible per a qualsevol persona amb accés al vostre dispositiu. Si utilitzeu aquesta opció, hauríeu d'assegurar-vos de mantenir el vostre dispositiu correctament protegit.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>Un o més dels URL introduïts no són vàlids. Reviseu-ho i torneu a provar de guardar-ho.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>No hem pogut processar la vostra sol·licitud. Torneu-ho a provar o contacteu amb nosaltres.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Permet capturar la pantalla</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Segur que voleu activar la captura de pantalla?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>Můj trezor</value>
|
<value>Můj trezor</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Authenticator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Název</value>
|
<value>Název</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Povoleno</value>
|
<value>Povoleno</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Off</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>On</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Stav</value>
|
<value>Stav</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Nelze přečíst ověřovací klíč.</value>
|
<value>Nelze přečíst ověřovací klíč.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Načtení proběhne automaticky.</value>
|
<value>Point your camera at the QR Code.
|
||||||
</data>
|
Scanning will happen automatically.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Namiřte fotoaparát na QR kód.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Načíst QR kód</value>
|
<value>Načíst QR kód</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Zkopírovat ověřovací kód (TOTP)</value>
|
<value>Zkopírovat ověřovací kód (TOTP)</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Pokud mají vaše přihlašovací údaje přidán autentizační klíč pro TOTP, vygenerovaný ověřovací kód (TOTP) se automaticky zkopíruje do schránky při každém automatickém vyplnění přihlašovacích údajů.</value>
|
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Zakázat automatické kopírování TOTP kódu</value>
|
<value>Copy TOTP automatically</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Pro použití této funkce je potřebné prémiové členství.</value>
|
<value>Pro použití této funkce je potřebné prémiové členství.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Expirace</value>
|
<value>Expirace</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Zakázat ikonky webových stránek</value>
|
<value>Show website icons</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Ikonky webových stránek zobrazí snadno rozeznatelný obrázek vedle každé položky ve vašem trezoru.</value>
|
<value>Show a recognizable image next to each login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>URL serveru ikonek</value>
|
<value>URL serveru ikonek</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Vyledat v kolekci</value>
|
<value>Vyledat v kolekci</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Vyhledat ve složce</value>
|
<value>Search File Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Typ hledání</value>
|
<value>Search Text Sends</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Hledat {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Typ</value>
|
<value>Typ</value>
|
||||||
@@ -1537,6 +1549,12 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Výchozí (Systémový)</value>
|
<value>Výchozí (Systémový)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Default dark theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Kopírovat poznámky</value>
|
<value>Kopírovat poznámky</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Černá</value>
|
<value>Černá</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>URI na černé listině</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Pro URI na černé listině nebude nabízeno automatické vyplnění. Záznamy oddělujte čárkou. Např.: „https://twitter.com, androidapp://com.twitter.android“.</value>
|
<value>Automatické vyplňování blokovaných URI</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Zakázat výzvu o uložení</value>
|
<value>Automatické vyplňování nebude nabídnuto pro blokované URI. Oddělte více URI čárkou. Například: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>„Výzva k uložení“ vás automaticky vyzve k uložení nových položek do trezoru, kdykoli je poprvé zadáte.</value>
|
<value>Ask to add login</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Ask to add an item if one isn't found in your vault.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Při restartu aplikace</value>
|
<value>Při restartu aplikace</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Přátelský název pro popis tohoto Send.</value>
|
<value>Přátelský název pro popis tohoto Send.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Text</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Text</value>
|
<value>Text</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Soubor, který chcete odeslat.</value>
|
<value>Soubor, který chcete odeslat.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>File type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>File type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Text type is selected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Text type is not selected, tap to select.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Datum odstranění</value>
|
<value>Datum odstranění</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2087,7 +2124,7 @@
|
|||||||
<value>Authenticate WebAuthn</value>
|
<value>Authenticate WebAuthn</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Fido2ReturnToApp" xml:space="preserve">
|
<data name="Fido2ReturnToApp" xml:space="preserve">
|
||||||
<value>Return to App</value>
|
<value>Zpět do aplikace</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Fido2CheckBrowser" xml:space="preserve">
|
<data name="Fido2CheckBrowser" xml:space="preserve">
|
||||||
<value>Please make sure your default browser supports WebAuthn and try again.</value>
|
<value>Please make sure your default browser supports WebAuthn and try again.</value>
|
||||||
@@ -2123,7 +2160,7 @@
|
|||||||
<value>Account Locked</value>
|
<value>Account Locked</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
|
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
|
||||||
<value>Account logged out successfully</value>
|
<value>Odhlášení proběhlo úspěšně</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccountRemovedSuccessfully" xml:space="preserve">
|
<data name="AccountRemovedSuccessfully" xml:space="preserve">
|
||||||
<value>Account removed successfully</value>
|
<value>Account removed successfully</value>
|
||||||
@@ -2171,9 +2208,109 @@
|
|||||||
<value>Ověřovací kód byl odeslán na váš e-mail</value>
|
<value>Ověřovací kód byl odeslán na váš e-mail</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain" xml:space="preserve">
|
<data name="AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain" xml:space="preserve">
|
||||||
<value>An error occurred while sending a verification code to your email. Please try again</value>
|
<value>Při odesílání ověřovacího kódu na váš e-mail došlo k chybě. Zkuste to prosím znovu</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Enter the verification code that was sent to your email</value>
|
<value>Zadejte ověřovací kód, který byl odeslán na %@</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Submit crash logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Options are expanded, tap to collapse.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Options are collapsed, tap to expand.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Uppercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Lowercase (A to Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Numbers (0 to 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Special Characters (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tap to go back</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Heslo je viditelné, klepněte pro skrytí</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Heslo není viditelné, klepněte pro zobrazení.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filter items by vault</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>All Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Vaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Vault: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>All</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Verification Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium subscription required</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Cannot add authenticator key? </value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Scan QR Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Cannot scan QR Code? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Authenticator Key</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Enter Key Manually</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Add TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Set up TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Once the key is successfully entered,
|
||||||
|
select Add TOTP to store the key safely</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>We were unable to process your request. Please try again or contact us.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Allow screen capture</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to enable Screen Capture?</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -299,6 +299,10 @@
|
|||||||
<value>Min boks</value>
|
<value>Min boks</value>
|
||||||
<comment>The title for the vault page.</comment>
|
<comment>The title for the vault page.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Authenticator" xml:space="preserve">
|
||||||
|
<value>Autentifikator</value>
|
||||||
|
<comment>Authenticator TOTP feature</comment>
|
||||||
|
</data>
|
||||||
<data name="Name" xml:space="preserve">
|
<data name="Name" xml:space="preserve">
|
||||||
<value>Navn</value>
|
<value>Navn</value>
|
||||||
<comment>Label for an entity name.</comment>
|
<comment>Label for an entity name.</comment>
|
||||||
@@ -770,6 +774,12 @@
|
|||||||
<data name="Enabled" xml:space="preserve">
|
<data name="Enabled" xml:space="preserve">
|
||||||
<value>Aktiveret</value>
|
<value>Aktiveret</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Off" xml:space="preserve">
|
||||||
|
<value>Fra</value>
|
||||||
|
</data>
|
||||||
|
<data name="On" xml:space="preserve">
|
||||||
|
<value>Til</value>
|
||||||
|
</data>
|
||||||
<data name="Status" xml:space="preserve">
|
<data name="Status" xml:space="preserve">
|
||||||
<value>Status</value>
|
<value>Status</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -889,11 +899,9 @@
|
|||||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||||
<value>Kan ikke læse autentificeringsnøgle.</value>
|
<value>Kan ikke læse autentificeringsnøgle.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||||
<value>Skanning vil ske automatisk.</value>
|
<value>Ret kameraet mod QR-koden.
|
||||||
</data>
|
Skanning vil ske automatisk.</value>
|
||||||
<data name="CameraInstructionTop" xml:space="preserve">
|
|
||||||
<value>Peg dit kamera mod QR-koden.</value>
|
|
||||||
</data>
|
</data>
|
||||||
<data name="ScanQrTitle" xml:space="preserve">
|
<data name="ScanQrTitle" xml:space="preserve">
|
||||||
<value>Skan QR-kode</value>
|
<value>Skan QR-kode</value>
|
||||||
@@ -907,11 +915,11 @@
|
|||||||
<data name="CopyTotp" xml:space="preserve">
|
<data name="CopyTotp" xml:space="preserve">
|
||||||
<value>Kopiér TOTP</value>
|
<value>Kopiér TOTP</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||||
<value>Hvis dit login har en autentificeringsnøgle tilknyttet, kopieres TOTP verifikationskoden automatisk til din udklipsholder når du auto-udfylder login.</value>
|
<value>Har et login en godkendelsesnøgle, så kopiér TOTP-bekræftelseskoden til udklipsholderen, når login auto-udfyldes.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||||
<value>Deaktivér automatisk TOTP kopiering</value>
|
<value>Kopiér TOTP automatisk</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PremiumRequired" xml:space="preserve">
|
<data name="PremiumRequired" xml:space="preserve">
|
||||||
<value>Premium-medlemskab kræves for at anvende denne funktion.</value>
|
<value>Premium-medlemskab kræves for at anvende denne funktion.</value>
|
||||||
@@ -1128,11 +1136,11 @@
|
|||||||
<data name="Expiration" xml:space="preserve">
|
<data name="Expiration" xml:space="preserve">
|
||||||
<value>Udløb</value>
|
<value>Udløb</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||||
<value>Slå webikoner fra</value>
|
<value>Vis webstedsikoner</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||||
<value>Webikoner vises som et genkendeligt billede ved siden af hvert loginelement i din boks.</value>
|
<value>Vis et genkendeligt billede ud for hvert login.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="IconsUrl" xml:space="preserve">
|
<data name="IconsUrl" xml:space="preserve">
|
||||||
<value>Ikonserver URL</value>
|
<value>Ikonserver URL</value>
|
||||||
@@ -1299,7 +1307,7 @@
|
|||||||
<value>1. Åbn iOS-appen "Indstillinger"</value>
|
<value>1. Åbn iOS-appen "Indstillinger"</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillTurnOn2" xml:space="preserve">
|
<data name="AutofillTurnOn2" xml:space="preserve">
|
||||||
<value>2. Tryk på "Adgangskoder & konti"</value>
|
<value>2. Tryk på "Adgangskoder"</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AutofillTurnOn3" xml:space="preserve">
|
<data name="AutofillTurnOn3" xml:space="preserve">
|
||||||
<value>3. Tryk på "Autoudfyld adgangskoder"</value>
|
<value>3. Tryk på "Autoudfyld adgangskoder"</value>
|
||||||
@@ -1311,7 +1319,7 @@
|
|||||||
<value>5. Vælg "Bitwarden"</value>
|
<value>5. Vælg "Bitwarden"</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="PasswordAutofill" xml:space="preserve">
|
<data name="PasswordAutofill" xml:space="preserve">
|
||||||
<value>Adgangskode Autoudfyld</value>
|
<value>Adgangskode autoudfyld</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
||||||
<value>Den letteste måde at tilføje nye logins til din boks er ved at bruge Bitwarden adgangskode Autoudfyld udvidelsen. Få mere at vide om brugen af Bitwarden adgangskode Autoudfyld udvidelsen ved at navigere til skærmbilledet "Indstillinger".</value>
|
<value>Den letteste måde at tilføje nye logins til din boks er ved at bruge Bitwarden adgangskode Autoudfyld udvidelsen. Få mere at vide om brugen af Bitwarden adgangskode Autoudfyld udvidelsen ved at navigere til skærmbilledet "Indstillinger".</value>
|
||||||
@@ -1372,11 +1380,15 @@
|
|||||||
<data name="SearchCollection" xml:space="preserve">
|
<data name="SearchCollection" xml:space="preserve">
|
||||||
<value>Søg i samling</value>
|
<value>Søg i samling</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchFolder" xml:space="preserve">
|
<data name="SearchFileSends" xml:space="preserve">
|
||||||
<value>Søg i mappe</value>
|
<value>Søg fil Sends</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="SearchType" xml:space="preserve">
|
<data name="SearchTextSends" xml:space="preserve">
|
||||||
<value>Søgetype</value>
|
<value>Søg tekst Sends</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchGroup" xml:space="preserve">
|
||||||
|
<value>Søg {0}</value>
|
||||||
|
<comment>ex: Search Logins</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="Type" xml:space="preserve">
|
<data name="Type" xml:space="preserve">
|
||||||
<value>Type</value>
|
<value>Type</value>
|
||||||
@@ -1537,8 +1549,14 @@
|
|||||||
<data name="ThemeDefault" xml:space="preserve">
|
<data name="ThemeDefault" xml:space="preserve">
|
||||||
<value>Standard (system)</value>
|
<value>Standard (system)</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||||
|
<value>Standard mørkt tema</value>
|
||||||
|
</data>
|
||||||
|
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||||
|
<value>Vælg det mørke tema, der skal bruges, når Standard (system) temaet bruges, mens din enheds mørke tilstand er aktiveret.</value>
|
||||||
|
</data>
|
||||||
<data name="CopyNotes" xml:space="preserve">
|
<data name="CopyNotes" xml:space="preserve">
|
||||||
<value>Kopiér notater</value>
|
<value>Kopiér notat</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Exit" xml:space="preserve">
|
<data name="Exit" xml:space="preserve">
|
||||||
<value>Afslut</value>
|
<value>Afslut</value>
|
||||||
@@ -1553,17 +1571,21 @@
|
|||||||
<value>Sort</value>
|
<value>Sort</value>
|
||||||
<comment>The color black</comment>
|
<comment>The color black</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUris" xml:space="preserve">
|
<data name="Nord" xml:space="preserve">
|
||||||
<value>Sortlistede URI'er</value>
|
<value>Nord</value>
|
||||||
|
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||||
<value>Sortlistede URI'er tilbyder ikke autoudfyldning. Listen skal være kommasepareret. F.eks: "https://twitter.com, androidapp: //com.twitter.android".</value>
|
<value>Autoudfyld blokerede URI'er</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePrompt" xml:space="preserve">
|
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
|
||||||
<value>Deaktivér gem-forespørgsel</value>
|
<value>Autoudfyldning tilbydes ikke for blokerede URI'er. Adskil flere URI'er med komma. F.eks.: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
<data name="AskToAddLogin" xml:space="preserve">
|
||||||
<value>"Gem"-forespørgslen beder dig automatisk gemme nye emner i din boks, når du angiver dem for første gang.</value>
|
<value>Spørg om at tilføje login</value>
|
||||||
|
</data>
|
||||||
|
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||||
|
<value>Spørg om at tilføje et element, hvis et ikke findes i din boks.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OnRestart" xml:space="preserve">
|
<data name="OnRestart" xml:space="preserve">
|
||||||
<value>Ved app-genstart</value>
|
<value>Ved app-genstart</value>
|
||||||
@@ -1802,13 +1824,13 @@
|
|||||||
<value>Autoudfyldtjeneste</value>
|
<value>Autoudfyldtjeneste</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InlineAutofill" xml:space="preserve">
|
<data name="InlineAutofill" xml:space="preserve">
|
||||||
<value>Brug indbygget Autoudfyld</value>
|
<value>Brug integreret autoudfyld</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="InlineAutofillDescription" xml:space="preserve">
|
<data name="InlineAutofillDescription" xml:space="preserve">
|
||||||
<value>Brug indbygget autofyld, hvis dit valgte IME (tastatur) understøtter det. Hvis din konfiguration ikke understøttes (eller denne indstilling er deaktiveret), benyttes standard overlejret Autoudfyld.</value>
|
<value>Brug indbygget autofyld, hvis dit valgte IME (tastatur) understøtter det. Hvis din konfiguration ikke understøttes (eller denne indstilling er deaktiveret), benyttes standard overlejret Autoudfyld.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Accessibility" xml:space="preserve">
|
<data name="Accessibility" xml:space="preserve">
|
||||||
<value>Brug Tilgængelighed</value>
|
<value>Brug tilgængelighed</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="AccessibilityDescription" xml:space="preserve">
|
<data name="AccessibilityDescription" xml:space="preserve">
|
||||||
<value>Brug Bitwarden-tilgængelighedstjenesten til at autoudfylde dine logins i apps og på internettet. Når aktiveret, vises en popup, når login-felter vælges.</value>
|
<value>Brug Bitwarden-tilgængelighedstjenesten til at autoudfylde dine logins i apps og på internettet. Når aktiveret, vises en popup, når login-felter vælges.</value>
|
||||||
@@ -1856,6 +1878,9 @@
|
|||||||
<value>Et venligt navn til at beskrive denne Send.</value>
|
<value>Et venligt navn til at beskrive denne Send.</value>
|
||||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Text" xml:space="preserve">
|
||||||
|
<value>Tekst</value>
|
||||||
|
</data>
|
||||||
<data name="TypeText" xml:space="preserve">
|
<data name="TypeText" xml:space="preserve">
|
||||||
<value>Tekst</value>
|
<value>Tekst</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1872,6 +1897,18 @@
|
|||||||
<data name="TypeFileInfo" xml:space="preserve">
|
<data name="TypeFileInfo" xml:space="preserve">
|
||||||
<value>Den fil, du vil sende.</value>
|
<value>Den fil, du vil sende.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Filtype er valgt.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FileTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Filtype er ikke valgt, tryk for at vælge.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsSelected" xml:space="preserve">
|
||||||
|
<value>Teksttype er valgt.</value>
|
||||||
|
</data>
|
||||||
|
<data name="TextTypeIsNotSelected" xml:space="preserve">
|
||||||
|
<value>Teksttype er ikke valgt, tryk for at vælge.</value>
|
||||||
|
</data>
|
||||||
<data name="DeletionDate" xml:space="preserve">
|
<data name="DeletionDate" xml:space="preserve">
|
||||||
<value>Sletningsdato</value>
|
<value>Sletningsdato</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2176,4 +2213,104 @@
|
|||||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||||
<value>Indtast den bekræftelseskode, der blev sendt til din e-mail</value>
|
<value>Indtast den bekræftelseskode, der blev sendt til din e-mail</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||||
|
<value>Indsend nedbrudslogger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SubmitCrashLogsDescription" xml:space="preserve">
|
||||||
|
<value>Hjælp Bitwarden med at forbedre app-stabiliteten ved at indsende nedbrudsrapporter.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsExpanded" xml:space="preserve">
|
||||||
|
<value>Indstillinger er udvidet, tryk for at kollapse.</value>
|
||||||
|
</data>
|
||||||
|
<data name="OptionsCollapsed" xml:space="preserve">
|
||||||
|
<value>Indstillinger er kollapset, tryk for at udvide.</value>
|
||||||
|
</data>
|
||||||
|
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Store bogstaver (A til Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="LowercaseAtoZ" xml:space="preserve">
|
||||||
|
<value>Små bogstaver (A til Z)</value>
|
||||||
|
</data>
|
||||||
|
<data name="NumbersZeroToNine" xml:space="preserve">
|
||||||
|
<value>Tal (0 til 9)</value>
|
||||||
|
</data>
|
||||||
|
<data name="SpecialCharacters" xml:space="preserve">
|
||||||
|
<value>Specialtegn (!@#$%^&*)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TapToGoBack" xml:space="preserve">
|
||||||
|
<value>Tryk for at gå tilbage</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
|
||||||
|
<value>Adgangskode er synlig, tryk for at skjule.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
|
||||||
|
<value>Adgangskode er ikke synlig, tryk for at vise.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FilterByVault" xml:space="preserve">
|
||||||
|
<value>Filtrér elementer efter boks</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllVaults" xml:space="preserve">
|
||||||
|
<value>Alle bokse</value>
|
||||||
|
</data>
|
||||||
|
<data name="Vaults" xml:space="preserve">
|
||||||
|
<value>Bokse</value>
|
||||||
|
</data>
|
||||||
|
<data name="VaultFilterDescription" xml:space="preserve">
|
||||||
|
<value>Boks: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="All" xml:space="preserve">
|
||||||
|
<value>Alle</value>
|
||||||
|
</data>
|
||||||
|
<data name="Totp" xml:space="preserve">
|
||||||
|
<value>TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationCodes" xml:space="preserve">
|
||||||
|
<value>Bekræftelseskoder</value>
|
||||||
|
</data>
|
||||||
|
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||||
|
<value>Premium-abonnement kræves</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||||
|
<value>Kan ikke tilføje autentificeringsnøgle?</value>
|
||||||
|
</data>
|
||||||
|
<data name="ScanQRCode" xml:space="preserve">
|
||||||
|
<value>Skan QR-kode</value>
|
||||||
|
</data>
|
||||||
|
<data name="CannotScanQRCode" xml:space="preserve">
|
||||||
|
<value>Kan ikke skanne QR-kode? </value>
|
||||||
|
</data>
|
||||||
|
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||||
|
<value>Autentificeringsnøgle</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnterKeyManually" xml:space="preserve">
|
||||||
|
<value>Angiv nøgle manuelt</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddTotp" xml:space="preserve">
|
||||||
|
<value>Tilføj TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetupTotp" xml:space="preserve">
|
||||||
|
<value>Opsæt TOTP</value>
|
||||||
|
</data>
|
||||||
|
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||||
|
<value>Når nøglen er angivet,
|
||||||
|
vælg Tilføj TOTP for at gemme nøglen sikkert</value>
|
||||||
|
</data>
|
||||||
|
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||||
|
<value></value>
|
||||||
|
</data>
|
||||||
|
<data name="NeverLockWarning" xml:space="preserve">
|
||||||
|
<value>Sættes låseindstillingen til “Aldrig”, er din boks tilgængelig for alle med adgang til enheden. Bruges denne mulighed, så vær sikker på, at din enhed er ordentligt beskyttet.</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||||
|
<value>En eller flere af de angivne URL'er er ugyldige. Ret fejlen(e) og prøv at gemme igen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="GenericErrorMessage" xml:space="preserve">
|
||||||
|
<value>Vi kunne ikke behandle din anmodning. Prøv venligst igen eller kontakt os.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowScreenCapture" xml:space="preserve">
|
||||||
|
<value>Tillad skærmoptagelse</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
|
<value>Er du sikker på, at du vil aktivere skærmoptagelse?</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user