Compare commits

..

29 Commits

Author SHA1 Message Date
TheLastProject
a3185e87fd Update feature graphic 2025-08-21 17:07:48 +00:00
il-Luca
8c4e7e7651 Create README.txt 2025-08-21 13:38:56 +02:00
il-Luca
33c8245d0d Rename ic_launcher.svg to ic_launcher_foreground.svg 2025-08-21 13:04:45 +02:00
il-Luca
c84378c647 Rename ic_launcher-foreground.xml to ic_launcher_foreground.xml 2025-08-21 12:52:44 +02:00
il-Luca
746dfb2a65 Delete app/src/main/res/drawable/ic_launcher_foreground.xml 2025-08-21 12:52:29 +02:00
il-Luca
d9495a3c9d Rename ic_launcher.xml to ic_launcher-foreground.xml 2025-08-21 12:46:17 +02:00
il-Luca
8e3a202efa Modified the script to generate feature graphics
- Noto Sans Kannada font size increased to 125 (it should still fit)
2025-08-21 09:41:27 +02:00
il-Luca
f184c6ac90 Modified the script to generate feature graphics
- replaced fonts: Lobster instead of Yesteryear, Lexenda instead of Lexenda Deca
- replaced serif version with sans version of Noto
2025-08-21 09:37:10 +02:00
il-Luca
6e242a9f86 Added new fonts 2025-08-21 09:24:43 +02:00
il-Luca
0394ab03a7 Added new fonts 2025-08-21 09:24:19 +02:00
il-Luca
71985f2b8a Added new fonts 2025-08-21 09:23:47 +02:00
il-Luca
5810ad7111 Deleted old fonts 2025-08-21 09:22:45 +02:00
il-Luca
3e10b8df68 Fixed FeatureGraphic color 2025-08-21 09:18:16 +02:00
il-Luca
c3bedc4132 Fixed icon color 2025-08-21 09:12:37 +02:00
il-Luca
662e2894e4 Fixed icon color 2025-08-21 09:11:09 +02:00
il-Luca
e6ad710e1c Fixed icon color 2025-08-21 09:10:04 +02:00
il-Luca
be1e629a1e Merge branch 'CatimaLoyalty:main' into main 2025-08-21 08:47:45 +02:00
il-Luca
7d65e9ae7e Updated icon.png file 2025-08-20 16:54:27 +02:00
il-Luca
9f5b7dd14b Deleted old logo.svg 2025-08-20 16:49:50 +02:00
il-Luca
61617d3f5d Added master design files to .design folder 2025-08-20 16:48:17 +02:00
il-Luca
74305386e7 Created .design folder 2025-08-20 16:47:40 +02:00
il-Luca
603015f194 Updated icon VectorDrawables 2025-08-20 16:42:26 +02:00
il-Luca
a7fcaf7479 Updated icon VectorDrawables 2025-08-20 16:41:54 +02:00
il-Luca
9170050762 Updated raster icon files 2025-08-20 16:38:43 +02:00
il-Luca
787499ba1f Updated raster icon files 2025-08-20 16:38:07 +02:00
il-Luca
e858e6662a Updated raster icon files 2025-08-20 16:37:32 +02:00
il-Luca
8bc54621fd Updated raster icon files 2025-08-20 16:36:45 +02:00
il-Luca
f053a6d58b Updated raster icon files 2025-08-20 16:32:19 +02:00
il-Luca
6e9de5f22f Updated Playstore icon 2025-08-20 09:42:24 +02:00
550 changed files with 11857 additions and 5008 deletions

View File

@@ -32,10 +32,10 @@ jobs:
matrix:
flavor: [Foss, Gplay]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v5.0.0
- name: Fail on bad translations
run: if grep -ri "<xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
- uses: gradle/actions/wrapper-validation@v5
- uses: gradle/actions/wrapper-validation@v4
- name: set up OpenJDK 21
run: |
sudo apt-get update
@@ -66,7 +66,7 @@ jobs:
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Archive test results
if: always()
uses: actions/upload-artifact@v5.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: test-results-flavor${{ matrix.flavor }}
path: app/build/reports

View File

@@ -1,5 +1,4 @@
name: Convert CHANGELOG to Fastlane
on:
workflow_dispatch:
push:
@@ -7,11 +6,20 @@ on:
- main
paths:
- 'CHANGELOG.md'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
convert_changelog_to_fastlane:
runs-on: ubuntu-latest
@@ -19,9 +27,9 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v5.0.0
- name: Setup Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@v5.6.0
with:
python-version: '3.x'
- name: Run converter script

View File

@@ -1,14 +1,22 @@
name: Write contributors to file
on:
workflow_dispatch:
schedule:
- cron: '3 4 * * 0'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
contributors_to_file:
runs-on: ubuntu-latest
@@ -17,7 +25,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v5.0.0
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0

View File

@@ -1,5 +1,4 @@
name: Generate feature graphic
on:
workflow_dispatch:
push:
@@ -8,16 +7,25 @@ on:
paths:
- 'fastlane/**/title.txt'
- '.scripts/generate_feature_graphic/**'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v5.0.0
- name: Install requirements
run: |
sudo apt-get update

View File

@@ -1,19 +0,0 @@
name: Update Gradle Wrapper
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: write
pull-requests: write
jobs:
update-gradle-wrapper:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2

View File

@@ -1,5 +1,4 @@
name: Update locales
on:
workflow_dispatch:
push:
@@ -8,16 +7,25 @@ on:
paths:
- app/src/main/res/values-*/strings.xml
- app/src/main/res/values/settings.xml
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v5.0.0
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales

44
.scripts/dump_stocard_stores.py Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/python3
import csv
import json
import msgpack
MSGPACK = "bootstrapdata.msgpack"
OUTFILE = "stocard_stores.csv"
def load(fh):
data = []
for r in msgpack.Unpacker(fh, raw=False):
if r["collection"] == "/loyalty-card-providers/":
d = json.loads(r["data"])
data.append([r["resource_id"], d["name"], d["default_barcode_format"]])
return data
def save(data, output_file=OUTFILE):
with open(output_file, "w") as fh:
writer = csv.writer(fh, lineterminator="\n")
writer.writerow(["_id", "name", "barcodeFormat"])
for row in data:
writer.writerow(row)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
epilog=f"INPUT_FILE must be a .msgpack or .apk and defaults to {MSGPACK}; "
f"OUTPUT_FILE defaults to {OUTFILE}")
parser.add_argument("input_file", metavar="INPUT_FILE", nargs="?", default=MSGPACK)
parser.add_argument("output_file", metavar="OUTPUT_FILE", nargs="?", default=OUTFILE)
args = parser.parse_args()
if args.input_file.lower().endswith(".apk"):
import zipfile
with zipfile.ZipFile(args.input_file) as zf:
with zf.open(f"assets/{MSGPACK}") as fh:
data = load(fh)
else:
with open(args.input_file, "rb") as fh:
data = load(fh)
save(data, args.output_file)

View File

@@ -1,8 +1,8 @@
<svg width="1024" height="500" viewBox="0 0 1024 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_78_203)">
<path d="M1024 0H0V500H1024V0Z" fill="#1F4262"/>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Lexend" font-size="35" letter-spacing="0em"><tspan x="481" y="325">Loyalty Card Wallet</tspan></text>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Lobster" font-size="150" letter-spacing="0em"><tspan x="469" y="270">Catima</tspan></text>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Lexend" font-size="35" letter-spacing="0em"><tspan x="481" y="325.125">Loyalty Card Wallet</tspan></text>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Lobster" font-size="150" letter-spacing="0em"><tspan x="469" y="270.25">Catima</tspan></text>
<g filter="url(#filter0_d_78_203)">
<path d="M218 156.307L308.21 123.473C316.514 120.45 325.696 124.732 328.718 133.035L339.663 163.106L234.417 201.412L218 156.307Z" fill="#F5A3A3"/>
<path d="M310.263 129.111C315.452 127.222 321.191 129.898 323.08 135.088L331.972 159.52L238.003 193.722L225.69 159.893L310.263 129.111Z" stroke="#E82E2E" stroke-width="12"/>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -36,19 +36,16 @@ for lang in "$script_location/../../fastlane/metadata/android/"*; do
# (Lobster and Lexend have limited language support)
case "$(basename "$lang")" in
bg|el-GR|ru-RU|uk) sed -i "s/Lexend/Noto Sans/" featureGraphic.svg ;;
ar|fa-IR) sed -i -e 's/svg direction="ltr"/svg direction="rtl"/' -e "s/Lobster/Noto Sans Arabic/" -e "s/Lexend/Noto Sans Arabic/" featureGraphic.svg ;;
he-IL) sed -i -e "s/Lobster/Noto Sans Hebrew/" -e "s/Lexend/Noto Sans Hebrew/" featureGraphic.svg ;;
fa-IR) sed -i -e 's/svg direction="ltr"/svg direction="rtl"/' -e "s/Lobster/Noto Sans Arabic/" -e "s/Lexend/Noto Sans Arabic/" featureGraphic.svg ;;
hi-IN) sed -i -e "s/Lobster/Noto Sans Devanagari/" -e "s/Lexend/Noto Sans Devanagari/" featureGraphic.svg ;;
ja-JP) sed -i "s/Lexend/Noto Sans CJK JP/" featureGraphic.svg ;;
kn-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e 's/\(<tspan x="469" \)y="270"/\1y="240"/' -e "s/Lobster/Noto Sans Kannada/" -e "s/Lexend/Noto Sans Kannada/" featureGraphic.svg ;;
kn-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e "s/Lobster/Noto Sans Kannada/" featureGraphic.svg ;;
ko) sed -i "s/Lexend/Noto Sans CJK KR/" featureGraphic.svg ;;
ta-IN) sed -i -e 's/font-size="150"/font-size="115"/' featureGraphic.svg ;;
zh-CN) sed -i "s/Lexend/Noto Sans CJK SC/" featureGraphic.svg ;;
zh-TW) sed -i -e "s/Lobster/Noto Sans CJK TC/" -e "s/Lexend/Noto Sans CJK TC/" featureGraphic.svg ;;
*) ;;
esac
fi
# Ensure images directory exists
mkdir -p images
# Generate .png (we use Inkscape because ImageMagick ignores RTL)

View File

@@ -1,27 +1,5 @@
# Changelog
## v2.39.1 - 154 (2025-10-01)
- Fix possible crash that could occur for cards missing colour information in the database
## v2.39.0 - 153 (2025-09-30)
- Target Android 16
- Fix possible crash after removing image from card
- Remove "Screen orientation" feature (Google removed the ability for apps to control screen rotation when targeting Android 16)
- Add crash reporter to FOSS build (not used in Google Play version, only in other app stores)
## v2.38.0 - 152 (2025-09-12)
- Add support for .pkpasses files
- Remove Stocard importer (Stocard no longer exists)
- Temporarily disable widget images below Android 12L (workaround for a crash issue)
## v2.37.0 - 151 (2025-08-22)
- New redesign of the Catima logo
- Translation updates
## v2.36.0 - 150 (2025-08-05)
- Add a widget showing all non-archived cards

View File

@@ -23,30 +23,6 @@ for good reason.
## Code Changes
Note: submitting LLM ("AI") generated code is strongly discouraged, as such
code is often (subtly) incorrect or overcomplicated (for example: unnecessarily
pulling in extra libraries for functionality already covered by existing
libraries). It also often makes unrelated changes that increase the risk of
introducing new issues and complicates reviewing. Even when it doesn't do any
of the before mentioned things, it will often not fit the coding style and flow
of existing code, requiring excessive refactoring.
While we cannot ever control or be sure if LLMs were used to generate the
submitted code, it is your responsibility to ensure that whatever code you
submit is correct and fits within the design of existing code. It is never
acceptable to defend a change by stating a LLM suggested it.
This is a personal plea more than anything: please understand that writing code
is the easy part. The hard part is making sure the code fits the design of the
rest of the application and is maintainable. Reviewing is a very time-consuming
task for this reason. Please do not use LLMs to quickly generate a "fix" and
moving the cost of labor to me as a reviewer. If you do use LLMs to generate
part of your code, please be open about this, explain what was generated how
and how you confirmed and refactored the code to fit the project and minimized
risk.
Please never submit LLM-generated code as-is.
### Test Your Code
There are four possible tests you can run to verify your code. The first

View File

@@ -1,5 +1,5 @@
**Last updated**
September 30 2025
August 30 2023
# Privacy Policy
Catima does not collect or transmit any personal information.
@@ -11,12 +11,6 @@ To ensure correct app functionality, we require access to the following:
Catima offers a feature to share cards with other users. All the relevant data is in the generated shareable URLs and never transmitted to our servers. When viewed through catima.app, the data in the URL is rendered using client-side Javascript to further ensure no data is ever transmitted to us.
## Crash reporting privacy
In the FOSS version of Catima (the version used on IzzyOnDroid, F-Droid and GitHub), the open source crash reporter ACRA is used for crash reporting. When a crash is detected, Catima will ask the user if they are willing to report the crash. If they choose to do so, the user's mail client is opened so they can review the data that would be sent. Crash reporting data is only sent when the user explicitly chooses to do so, it is **never** sent automatically. Crash reporting data is only used to solve crashes and no (potentially) sensitive information is ever shared. Users who do not want to be asked to report crashes can disable the "Ask to send crash reports" setting in Catima settings.
For the Google Play version of Catima, crash reporting is [managed by Google](https://support.google.com/googleplay/android-developer/answer/9859174?hl=en). Users can opt in or out of crash reporting through the Google app under the "Usage and diagnostics" setting.
# Changes
This Privacy Policy may be updated from time to time for any reason. We will notify you of any changes to our Privacy Policy by posting the new Privacy Policy to https://catima.app/privacy-policy/. A snapshot of the Privacy Policy is available within the Catima app, though it may be outdated. When the Privacy Policy on the website and in the app differ, the website should be considered leading. You are advised to consult the Privacy Policy regularly for any changes, as continued use is deemed approval of all changes.

View File

@@ -1,8 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
alias(libs.plugins.com.android.application)
alias(libs.plugins.org.jetbrains.kotlin.android)
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
kotlin {
@@ -11,14 +11,14 @@ kotlin {
android {
namespace = "protect.card_locker"
compileSdk = 36
compileSdk = 35
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk = 21
targetSdk = 36
versionCode = 154
versionName = "2.39.1"
targetSdk = 35
versionCode = 150
versionName = "2.36.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -29,7 +29,6 @@ android {
buildConfigField("boolean", "showDonate", "true")
buildConfigField("boolean", "showRateOnGooglePlay", "false")
buildConfigField("boolean", "useAcraCrashReporter", "true")
}
buildTypes {
@@ -62,9 +61,6 @@ android {
// Google doesn't allow donation links
buildConfigField("boolean", "showDonate", "false")
buildConfigField("boolean", "showRateOnGooglePlay", "true")
// Google Play already sends crashes to the Google Play Console
buildConfigField("boolean", "useAcraCrashReporter", "false")
}
}
@@ -113,38 +109,38 @@ android {
dependencies {
// AndroidX
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.constraintlayout.constraintlayout)
implementation(libs.androidx.core.core.ktx)
implementation(libs.androidx.core.core.remoteviews)
implementation(libs.androidx.core.core.splashscreen)
implementation(libs.androidx.exifinterface.exifinterface)
implementation(libs.androidx.palette.palette)
implementation(libs.androidx.preference.preference)
implementation(libs.com.google.android.material.material)
coreLibraryDesugaring(libs.com.android.tools.desugar.jdk.libs)
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.core:core-remoteviews:1.1.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.exifinterface:exifinterface:1.4.1")
implementation("androidx.palette:palette:1.0.0")
implementation("androidx.preference:preference:1.2.1")
implementation("com.google.android.material:material:1.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
// Third-party
implementation(libs.com.journeyapps.zxing.android.embedded)
implementation(libs.com.github.yalantis.ucrop)
implementation(libs.com.google.zxing.core)
implementation(libs.org.apache.commons.commons.csv)
implementation(libs.com.jaredrummler.colorpicker)
implementation(libs.net.lingala.zip4j.zip4j)
// Crash reporting
implementation(libs.bundles.acra)
implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar")
implementation("com.github.yalantis:ucrop:2.2.10")
implementation("com.google.zxing:core:3.5.3")
implementation("org.apache.commons:commons-csv:1.9.0")
implementation("com.jaredrummler:colorpicker:1.1.0")
implementation("net.lingala.zip4j:zip4j:2.11.5")
// Testing
testImplementation(libs.androidx.test.core)
testImplementation(libs.junit.junit)
testImplementation(libs.org.robolectric.robolectric)
val androidXTestVersion = "1.7.0"
val junitVersion = "4.13.2"
testImplementation("androidx.test:core:$androidXTestVersion")
testImplementation("junit:junit:$junitVersion")
testImplementation("org.robolectric:robolectric:4.15.1")
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.junit.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.uiautomator.uiautomator)
androidTestImplementation(libs.androidx.test.espresso.espresso.core)
androidTestImplementation("androidx.test:core:$androidXTestVersion")
androidTestImplementation("junit:junit:$junitVersion")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test:runner:$androidXTestVersion")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}
tasks.register("copyRawResFiles", Copy::class) {

View File

@@ -21,19 +21,4 @@
-keepattributes SourceFile,LineNumberTable
# This keep the class and method names the same, for debugging stack traces
-dontobfuscate
# Required for uCrop 2.2.11
# This is generated automatically by the Android Gradle plugin.
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.SupportedOptions
-dontwarn okhttp3.Call
-dontwarn okhttp3.Dispatcher
-dontwarn okhttp3.OkHttpClient
-dontwarn okhttp3.Request$Builder
-dontwarn okhttp3.Request
-dontwarn okhttp3.Response
-dontwarn okhttp3.ResponseBody
-dontwarn okio.BufferedSource
-dontwarn okio.Okio
-dontwarn okio.Sink
-dontobfuscate

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">卡提碼除錯版</string>
</resources>
<string name="app_name">Catima 除錯版</string>
</resources>

View File

@@ -4,7 +4,7 @@
<permission
android:description="@string/permissionReadCardsDescription"
android:icon="@drawable/ic_launcher_monochrome"
android:icon="@drawable/ic_launcher_foreground"
android:label="@string/permissionReadCardsLabel"
android:name="${applicationId}.READ_CARDS"
android:protectionLevel="dangerous" />
@@ -65,7 +65,6 @@
<data android:mimeType="application/vnd.apple.pkpass" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
<data android:mimeType="application/vnd.apple.pkpasses" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -77,7 +76,6 @@
<data android:mimeType="application/vnd.apple.pkpass" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
<data android:mimeType="application/vnd.apple.pkpasses" />
</intent-filter>
</activity>
<activity
@@ -144,11 +142,12 @@
android:name=".preferences.SettingsActivity"
android:label="@string/settings"
android:theme="@style/AppTheme.NoActionBar" />
<!-- FIXME: ImportExportActivity cancels import on rotation -->
<!-- FIXME: locked screenOrientation is a workaround for https://github.com/CatimaLoyalty/Android/issues/1715, remove when https://github.com/CatimaLoyalty/Android/issues/513 is fixed -->
<activity
android:name=".ImportExportActivity"
android:label="@string/importExport"
android:exported="true"
android:screenOrientation="locked"
android:theme="@style/AppTheme.NoActionBar">
<!-- ZIP Intent Filter -->

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -99,9 +99,9 @@ public class AboutContent {
public String getThirdPartyLibraries() {
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
usedLibraries.add(new ThirdPartyInfo("ACRA", "https://github.com/ACRA/acra", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
usedLibraries.add(new ThirdPartyInfo("uCrop", "https://github.com/Yalantis/uCrop", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Zip4j", "https://github.com/srikanth-lingala/zip4j", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("ZXing", "https://github.com/zxing/zxing", "Apache 2.0"));

View File

@@ -0,0 +1,5 @@
package protect.card_locker;
public interface BarcodeImageWriterResultCallback {
void onBarcodeImageWriterResult(boolean success);
}

View File

@@ -1,5 +0,0 @@
package protect.card_locker
interface BarcodeImageWriterResultCallback {
fun onBarcodeImageWriterResult(success: Boolean)
}

View File

@@ -0,0 +1,402 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import protect.card_locker.async.TaskHandler;
import protect.card_locker.databinding.ImportExportActivityBinding;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
public class ImportExportActivity extends CatimaAppCompatActivity {
private ImportExportActivityBinding binding;
private static final String TAG = "Catima";
private ImportExportTask importExporter;
private String importAlertTitle;
private String importAlertMessage;
private DataFormat importDataFormat;
private String exportPassword;
private ActivityResultLauncher<Intent> fileCreateLauncher;
private ActivityResultLauncher<String> fileOpenLauncher;
final private TaskHandler mTasks = new TaskHandler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ImportExportActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.importExport);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
Intent fileIntent = getIntent();
if (fileIntent != null && fileIntent.getType() != null) {
chooseImportType(fileIntent.getData());
}
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
fileCreateLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Intent intent = result.getData();
if (intent == null) {
Log.e(TAG, "Activity returned NULL data");
return;
}
Uri uri = intent.getData();
if (uri == null) {
Log.e(TAG, "Activity returned NULL uri");
return;
}
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
new Thread() {
@Override
public void run() {
try {
OutputStream writer = getContentResolver().openOutputStream(uri);
Log.d(TAG, "Starting file export with: " + result);
startExport(writer, uri, exportPassword.toCharArray(), true);
} catch (IOException e) {
Log.e(TAG, "Failed to export file: " + result, e);
onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
}
}
}.start();
});
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
if (result == null) {
Log.e(TAG, "Activity returned NULL data");
return;
}
openFileForImport(result, null);
});
// Check that there is a file manager available
final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE);
intentCreateDocumentAction.setType("application/zip");
intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip");
Button exportButton = binding.exportButton;
exportButton.setOnClickListener(v -> {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ImportExportActivity.this);
builder.setTitle(R.string.exportPassword);
FrameLayout container = new FrameLayout(ImportExportActivity.this);
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(50, 10, 50, 0);
textInputLayout.setLayoutParams(params);
final EditText input = new EditText(ImportExportActivity.this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
textInputLayout.addView(input);
container.addView(textInputLayout);
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
exportPassword = input.getText().toString();
try {
fileCreateLauncher.launch(intentCreateDocumentAction);
} catch (ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
});
// Check that there is a file manager available
Button importFilesystem = binding.importOptionFilesystemButton;
importFilesystem.setOnClickListener(v -> chooseImportType(null));
// FIXME: The importer/exporter is currently quite broken
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void openFileForImport(Uri uri, char[] password) {
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
new Thread() {
@Override
public void run() {
try {
InputStream reader = getContentResolver().openInputStream(uri);
Log.d(TAG, "Starting file import with: " + uri);
startImport(reader, uri, importDataFormat, password, true);
} catch (IOException e) {
Log.e(TAG, "Failed to import file: " + uri, e);
onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
}
}
}.start();
}
private void chooseImportType(@Nullable Uri fileData) {
List<CharSequence> betaImportOptions = new ArrayList<>();
betaImportOptions.add("Fidme");
betaImportOptions.add("Stocard");
List<CharSequence> importOptions = new ArrayList<>();
for (String importOption : getResources().getStringArray(R.array.import_types_array)) {
if (betaImportOptions.contains(importOption)) {
importOption = importOption + " (BETA)";
}
importOptions.add(importOption);
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.chooseImportType)
.setItems(importOptions.toArray(new CharSequence[importOptions.size()]), (dialog, which) -> {
switch (which) {
// Catima
case 0:
importAlertTitle = getString(R.string.importCatima);
importAlertMessage = getString(R.string.importCatimaMessage);
importDataFormat = DataFormat.Catima;
break;
// Fidme
case 1:
importAlertTitle = getString(R.string.importFidme);
importAlertMessage = getString(R.string.importFidmeMessage);
importDataFormat = DataFormat.Fidme;
break;
// Loyalty Card Keychain
case 2:
importAlertTitle = getString(R.string.importLoyaltyCardKeychain);
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage);
importDataFormat = DataFormat.Catima;
break;
// Stocard
case 3:
importAlertTitle = getString(R.string.importStocard);
importAlertMessage = getString(R.string.importStocardMessage);
importDataFormat = DataFormat.Stocard;
break;
// Voucher Vault
case 4:
importAlertTitle = getString(R.string.importVoucherVault);
importAlertMessage = getString(R.string.importVoucherVaultMessage);
importDataFormat = DataFormat.VoucherVault;
break;
default:
throw new IllegalArgumentException("Unknown DataFormat");
}
if (fileData != null) {
openFileForImport(fileData, null);
return;
}
new MaterialAlertDialogBuilder(this)
.setTitle(importAlertTitle)
.setMessage(importAlertMessage)
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
try {
fileOpenLauncher.launch("*/*");
} catch (ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
});
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onImportComplete(result, targetUri, dataFormat);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
}
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onExportComplete(result, targetUri);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
DataFormat.Catima, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
}
@Override
protected void onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.passwordRequired);
FrameLayout container = new FrameLayout(ImportExportActivity.this);
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(50, 10, 50, 0);
textInputLayout.setLayoutParams(params);
final EditText input = new EditText(ImportExportActivity.this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
textInputLayout.addView(input);
container.addView(textInputLayout);
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
openFileForImport(uri, input.getText().toString().toCharArray());
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
}
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
int messageId;
if (result.resultType() == ImportExportResultType.Success) {
messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful;
} else {
messageId = isImport ? R.string.importFailed : R.string.exportFailed;
}
StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId));
if (result.developerDetails() != null) {
messageBuilder.append("\n\n");
messageBuilder.append(getResources().getString(R.string.include_if_asking_support));
messageBuilder.append("\n\n");
messageBuilder.append(result.developerDetails());
}
return messageBuilder.toString();
}
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
ImportExportResultType resultType = result.resultType();
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat, path);
return;
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
builder.setMessage(buildResultDialogMessage(result, true));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
builder.create().show();
}
private void onExportComplete(ImportExportResult result, final Uri path) {
ImportExportResultType resultType = result.resultType();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
builder.setMessage(buildResultDialogMessage(result, false));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
if (resultType == ImportExportResultType.Success) {
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
});
}
builder.create().show();
}
}

View File

@@ -1,416 +0,0 @@
package protect.card_locker
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.MenuItem
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import protect.card_locker.async.TaskHandler
import protect.card_locker.databinding.ImportExportActivityBinding
import protect.card_locker.importexport.DataFormat
import protect.card_locker.importexport.ImportExportResult
import protect.card_locker.importexport.ImportExportResultType
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class ImportExportActivity : CatimaAppCompatActivity() {
private lateinit var binding: ImportExportActivityBinding
private var importExporter: ImportExportTask? = null
private var importAlertTitle: String? = null
private var importAlertMessage: String? = null
private var importDataFormat: DataFormat? = null
private var exportPassword: String? = null
private lateinit var fileCreateLauncher: ActivityResultLauncher<Intent>
private lateinit var fileOpenLauncher: ActivityResultLauncher<String>
private val mTasks = TaskHandler()
companion object {
private const val TAG = "Catima"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ImportExportActivityBinding.inflate(layoutInflater)
setTitle(R.string.importExport)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
val toolbar: Toolbar = binding.toolbar
setSupportActionBar(toolbar)
enableToolbarBackButton()
val fileIntent = intent
if (fileIntent?.type != null) {
chooseImportType(fileIntent.data)
}
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
fileCreateLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val intent = result.data
if (intent == null) {
Log.e(TAG, "Activity returned NULL data")
return@registerForActivityResult
}
val uri = intent.data
if (uri == null) {
Log.e(TAG, "Activity returned NULL uri")
return@registerForActivityResult
}
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
Thread {
try {
val writer = contentResolver.openOutputStream(uri)
Log.d(TAG, "Starting file export with: $result")
startExport(writer, uri, exportPassword?.toCharArray(), true)
} catch (e: IOException) {
Log.e(TAG, "Failed to export file: $result", e)
onExportComplete(
ImportExportResult(
ImportExportResultType.GenericFailure,
result.toString()
), uri
)
}
}.start()
}
fileOpenLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
if (result == null) {
Log.e(TAG, "Activity returned NULL data")
return@registerForActivityResult
}
openFileForImport(result, null)
}
// Check that there is a file manager available
val intentCreateDocumentAction = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip"
putExtra(Intent.EXTRA_TITLE, "catima.zip")
}
val exportButton: Button = binding.exportButton
exportButton.setOnClickListener {
val builder = MaterialAlertDialogBuilder(this@ImportExportActivity)
builder.setTitle(R.string.exportPassword)
val container = FrameLayout(this@ImportExportActivity)
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(50, 10, 50, 0)
}
}
val input = EditText(this@ImportExportActivity).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
setHint(R.string.exportPasswordHint)
}
textInputLayout.addView(input)
container.addView(textInputLayout)
builder.setView(container)
builder.setPositiveButton(R.string.ok) { _, _ ->
exportPassword = input.text.toString()
try {
fileCreateLauncher.launch(intentCreateDocumentAction)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
R.string.failedOpeningFileManager,
Toast.LENGTH_LONG
).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
builder.show()
}
// Check that there is a file manager available
val importFilesystem: Button = binding.importOptionFilesystemButton
importFilesystem.setOnClickListener { chooseImportType(null) }
// FIXME: The importer/exporter is currently quite broken
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun openFileForImport(uri: Uri, password: CharArray?) {
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
Thread {
try {
val reader = contentResolver.openInputStream(uri)
Log.d(TAG, "Starting file import with: $uri")
startImport(reader, uri, importDataFormat, password, true)
} catch (e: IOException) {
Log.e(TAG, "Failed to import file: $uri", e)
onImportComplete(
ImportExportResult(
ImportExportResultType.GenericFailure,
e.toString()
), uri, importDataFormat
)
}
}.start()
}
private fun chooseImportType(fileData: Uri?) {
val betaImportOptions = mutableListOf<CharSequence>()
betaImportOptions.add("Fidme")
val importOptions = mutableListOf<CharSequence>()
for (importOption in resources.getStringArray(R.array.import_types_array)) {
var option = importOption
if (betaImportOptions.contains(importOption)) {
option = "$importOption (BETA)"
}
importOptions.add(option)
}
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(R.string.chooseImportType)
.setItems(importOptions.toTypedArray()) { _, which ->
when (which) {
// Catima
0 -> {
importAlertTitle = getString(R.string.importCatima)
importAlertMessage = getString(R.string.importCatimaMessage)
importDataFormat = DataFormat.Catima
}
// Fidme
1 -> {
importAlertTitle = getString(R.string.importFidme)
importAlertMessage = getString(R.string.importFidmeMessage)
importDataFormat = DataFormat.Fidme
}
// Loyalty Card Keychain
2 -> {
importAlertTitle = getString(R.string.importLoyaltyCardKeychain)
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage)
importDataFormat = DataFormat.Catima
}
// Voucher Vault
3 -> {
importAlertTitle = getString(R.string.importVoucherVault)
importAlertMessage = getString(R.string.importVoucherVaultMessage)
importDataFormat = DataFormat.VoucherVault
}
else -> throw IllegalArgumentException("Unknown DataFormat")
}
if (fileData != null) {
openFileForImport(fileData, null)
return@setItems
}
MaterialAlertDialogBuilder(this)
.setTitle(importAlertTitle)
.setMessage(importAlertMessage)
.setPositiveButton(R.string.ok) { _, _ ->
try {
fileOpenLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
R.string.failedOpeningFileManager,
Toast.LENGTH_LONG
).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
builder.show()
}
private fun startImport(
target: InputStream?,
targetUri: Uri,
dataFormat: DataFormat?,
password: CharArray?,
closeWhenDone: Boolean
) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
onImportComplete(result, targetUri, dataFormat)
if (closeWhenDone) {
try {
target?.close()
} catch (ioException: IOException) {
ioException.printStackTrace()
}
}
}
importExporter = ImportExportTask(
this@ImportExportActivity,
dataFormat, target, password, listener
)
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter)
}
private fun startExport(
target: OutputStream?,
targetUri: Uri,
password: CharArray?,
closeWhenDone: Boolean
) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
onExportComplete(result, targetUri)
if (closeWhenDone) {
try {
target?.close()
} catch (ioException: IOException) {
ioException.printStackTrace()
}
}
}
importExporter = ImportExportTask(
this@ImportExportActivity,
DataFormat.Catima, target, password, listener
)
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter)
}
override fun onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
private fun retryWithPassword(dataFormat: DataFormat, uri: Uri) {
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(R.string.passwordRequired)
val container = FrameLayout(this@ImportExportActivity)
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(50, 10, 50, 0)
}
}
val input = EditText(this@ImportExportActivity).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
setHint(R.string.exportPasswordHint)
}
textInputLayout.addView(input)
container.addView(textInputLayout)
builder.setView(container)
builder.setPositiveButton(R.string.ok) { _, _ ->
openFileForImport(uri, input.text.toString().toCharArray())
}
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
builder.show()
}
private fun buildResultDialogMessage(result: ImportExportResult, isImport: Boolean): String {
val messageId = if (result.resultType() == ImportExportResultType.Success) {
if (isImport) R.string.importSuccessful else R.string.exportSuccessful
} else {
if (isImport) R.string.importFailed else R.string.exportFailed
}
val messageBuilder = StringBuilder(resources.getString(messageId))
if (result.developerDetails() != null) {
messageBuilder.append("\n\n")
messageBuilder.append(resources.getString(R.string.include_if_asking_support))
messageBuilder.append("\n\n")
messageBuilder.append(result.developerDetails())
}
return messageBuilder.toString()
}
private fun onImportComplete(result: ImportExportResult, path: Uri, dataFormat: DataFormat?) {
val resultType = result.resultType()
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat!!, path)
return
}
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.importSuccessfulTitle else R.string.importFailedTitle)
builder.setMessage(buildResultDialogMessage(result, true))
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
builder.create().show()
}
private fun onExportComplete(result: ImportExportResult, path: Uri) {
val resultType = result.resultType()
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.exportSuccessfulTitle else R.string.exportFailedTitle)
builder.setMessage(buildResultDialogMessage(result, false))
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
if (resultType == ImportExportResultType.Success) {
val sendLabel = this@ImportExportActivity.resources.getText(R.string.sendLabel)
builder.setPositiveButton(sendLabel) { dialog, _ ->
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, path)
type = "text/csv"
// set flag to give temporary permission to external app to use the FileProvider
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
this@ImportExportActivity.startActivity(Intent.createChooser(sendIntent, sendLabel))
dialog.dismiss()
}
}
builder.create().show()
}
}

View File

@@ -1,17 +1,12 @@
package protect.card_locker;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
@@ -37,7 +32,7 @@ public class ImportExportTask implements CompatCallable<ImportExportResult> {
private char[] password;
private TaskCompleteListener listener;
private AlertDialog progress;
private ProgressDialog progress;
/**
* Constructor which will setup a task for exporting to the given file
@@ -93,36 +88,12 @@ public class ImportExportTask implements CompatCallable<ImportExportResult> {
}
public void onPreExecute() {
MaterialAlertDialogBuilder progressDialogBuilder = new MaterialAlertDialogBuilder(activity);
progressDialogBuilder.setCancelable(false); // Don't cancel if user taps next to dialog
progressDialogBuilder.setTitle(doImport ? R.string.importing : R.string.exporting);
progress = new ProgressDialog(activity);
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
// Create components
TextView progressDialogTextView = new TextView(activity);
progressDialogTextView.setText(R.string.pleaseDoNotRotateTheDevice); // FIXME: Instead of telling the user to not rotate, rotation should not cancel the import
ProgressBar progressDialogProgressBar = new ProgressBar(activity);
progressDialogProgressBar.setIndeterminate(true);
progress.setOnCancelListener(dialog -> cancel());
progress.setOnDismissListener(dialog -> cancel());
// Create LinearLayout (to put the components below each other)
LinearLayout progressDialogLayout = new LinearLayout(activity);
progressDialogLayout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams progressDialogLayoutParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
int contentPadding = activity.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
progressDialogLayoutParams.setMargins(contentPadding, contentPadding / 2, contentPadding, 0);
// Put components in layout
progressDialogLayout.addView(progressDialogTextView, progressDialogLayoutParams);
progressDialogLayout.addView(progressDialogProgressBar, progressDialogLayoutParams);
// Create and show dialog
progressDialogBuilder.setView(progressDialogLayout);
progressDialogBuilder.setNeutralButton(R.string.cancel, (dialogInterface, i) -> cancel());
progressDialogBuilder.setOnCancelListener(dialogInterface -> cancel());
progressDialogBuilder.setOnDismissListener(dialogInterface -> cancel());
progress = progressDialogBuilder.create();
progress.show();
}

View File

@@ -22,6 +22,11 @@ import androidx.core.graphics.PaintCompat;
* is shown instead.
*/
class LetterBitmap {
/**
* The number of available tile colors
*/
private static final int NUM_OF_TILE_COLORS = 8;
/**
* The letter bitmap
*/
@@ -116,7 +121,7 @@ class LetterBitmap {
private static int pickColor(String key, TypedArray colors) {
// String.hashCode() is not supposed to change across java versions, so
// this should guarantee the same key always maps to the same color
final int color = Math.abs(key.hashCode()) % colors.length();
final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS;
return colors.getColor(color, Color.BLACK);
}

View File

@@ -101,8 +101,7 @@ class ListWidget : AppWidgetProvider() {
setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor)
val icon = loyaltyCard.getImageThumbnail(context)
// setImageViewIcon is not supported on Android 5, so force Android 5 down the text path
// FIXME: The icon flow causes a crash up to Android 12L, so SDK_INT is forced up from 23 to 33
if (icon != null && Build.VERSION.SDK_INT >= 32) {
if (icon != null && Build.VERSION.SDK_INT >= 23) {
setInt(R.id.item_container_foreground, "setBackgroundColor", foreground)
setImageViewIcon(R.id.item_image, Icon.createWithBitmap(icon))
setViewVisibility(R.id.item_text, View.INVISIBLE)

View File

@@ -719,6 +719,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
int colorBackground = MaterialColors.getColor(this, android.R.attr.colorBackground, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
mCropperOptions.setToolbarColor(colorSurface);
mCropperOptions.setStatusBarColor(colorSurface);
mCropperOptions.setToolbarWidgetColor(colorOnSurface);
mCropperOptions.setRootViewBackgroundColor(colorBackground);
// set tool tip to be the darker of primary color

View File

@@ -4,12 +4,6 @@ import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.DialogConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.data.StringFormat;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardLockerApplication extends Application {
@@ -18,27 +12,6 @@ public class LoyaltyCardLockerApplication extends Application {
public void onCreate() {
super.onCreate();
// Initialize crash reporter (if enabled)
if (BuildConfig.useAcraCrashReporter) {
ACRA.init(this, new CoreConfigurationBuilder()
//core configuration:
.withBuildConfigClass(BuildConfig.class)
.withReportFormat(StringFormat.KEY_VALUE_LIST)
.withPluginConfigurations(
new DialogConfigurationBuilder()
.withText(String.format(getString(R.string.acra_catima_has_crashed), getString(R.string.app_name)))
.withCommentPrompt(getString(R.string.acra_explain_crash))
.withResTheme(R.style.AppTheme)
.build(),
new MailSenderConfigurationBuilder()
.withMailTo("acra-crash@catima.app")
.withSubject(String.format(getString(R.string.acra_crash_email_subject), getString(R.string.app_name)))
.build()
)
);
}
// Set theme
Settings settings = new Settings(this);
AppCompatDelegate.setDefaultNightMode(settings.getTheme());
}

View File

@@ -262,6 +262,19 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
settings = new Settings(this);
String cardOrientation = settings.getCardViewOrientation();
if (cardOrientation.equals(getString(R.string.settings_key_follow_sensor_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
} else if (cardOrientation.equals(getString(R.string.settings_key_lock_on_opening_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
} else if (cardOrientation.equals(getString(R.string.settings_key_portrait_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else if (cardOrientation.equals(getString(R.string.settings_key_landscape_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
if (savedInstanceState != null) {
mainImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX);
isFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN);
@@ -1085,12 +1098,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
}
private void setMainImagePreviousNextButtons() {
// Ensure the main image index is valid. After a card update, some images (front/back/barcode)
// may have been removed, so the index should not exceed the number of available images.
if(mainImageIndex > imageTypes.size() - 1){
mainImageIndex = 0;
}
if (imageTypes.size() < 2) {
binding.mainLeftButton.setVisibility(View.INVISIBLE);
binding.mainRightButton.setVisibility(View.INVISIBLE);

View File

@@ -498,8 +498,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else if (receivedType.equals("application/vnd.apple.pkpasses")) {
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data);
} else {
Log.e(TAG, "Wrong mime-type");
return;

View File

@@ -0,0 +1,242 @@
package protect.card_locker;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import protect.card_locker.databinding.ActivityManageGroupBinding;
public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener {
private ActivityManageGroupBinding binding;
private SQLiteDatabase mDatabase;
private ManageGroupCursorAdapter mAdapter;
private final String SAVE_INSTANCE_ADAPTER_STATE = "adapterState";
private final String SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName";
protected Group mGroup = null;
private RecyclerView mCardList;
private TextView noGroupCardsText;
private EditText mGroupNameText;
private boolean mGroupNameNotInUse;
@Override
protected void onCreate(Bundle inputSavedInstanceState) {
super.onCreate(inputSavedInstanceState);
binding = ActivityManageGroupBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsetsAndFabOffset(binding.getRoot(), binding.fabSave);
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
mDatabase = new DBHelper(this).getWritableDatabase();
noGroupCardsText = binding.include.noGroupCardsText;
mCardList = binding.include.list;
FloatingActionButton saveButton = binding.fabSave;
mGroupNameText = binding.editTextGroupName;
mGroupNameText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
mGroupNameNotInUse = true;
mGroupNameText.setError(null);
String currentGroupName = mGroupNameText.getText().toString().trim();
if (currentGroupName.length() == 0) {
mGroupNameText.setError(getResources().getText(R.string.group_name_is_empty));
return;
}
if (!mGroup._id.equals(currentGroupName)) {
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
mGroupNameNotInUse = false;
mGroupNameText.setError(getResources().getText(R.string.group_name_already_in_use));
} else {
mGroupNameNotInUse = true;
}
}
}
});
Intent intent = getIntent();
String groupId = intent.getStringExtra("group");
if (groupId == null) {
throw (new IllegalArgumentException("this activity expects a group loaded into it's intent"));
}
Log.d("groupId", "groupId: " + groupId);
mGroup = DBHelper.getGroup(mDatabase, groupId);
if (mGroup == null) {
throw (new IllegalArgumentException("cannot load group " + groupId + " from database"));
}
mGroupNameText.setText(mGroup._id);
setTitle(getString(R.string.editGroup, mGroup._id));
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup, null);
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
if (inputSavedInstanceState != null) {
mAdapter.importInGroupState(integerArrayToAdapterState(inputSavedInstanceState.getIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE)));
mGroupNameText.setText(inputSavedInstanceState.getString(SAVE_INSTANCE_CURRENT_GROUP_NAME));
}
enableToolbarBackButton();
saveButton.setOnClickListener(v -> {
String currentGroupName = mGroupNameText.getText().toString().trim();
if (!currentGroupName.equals(mGroup._id)) {
if (currentGroupName.length() == 0) {
Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show();
return;
}
if (!mGroupNameNotInUse) {
Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show();
return;
}
}
mAdapter.commitToDatabase();
if (!currentGroupName.equals(mGroup._id)) {
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName);
}
Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT).show();
finish();
});
// this setText is here because content_main.xml is reused from main activity
noGroupCardsText.setText(getResources().getText(R.string.noGiftCardsGroup));
updateLoyaltyCardList();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
leaveWithoutSaving();
}
});
}
private ArrayList<Integer> adapterStateToIntegerArray(HashMap<Integer, Boolean> adapterState) {
ArrayList<Integer> ret = new ArrayList<>(adapterState.size() * 2);
for (Map.Entry<Integer, Boolean> entry : adapterState.entrySet()) {
ret.add(entry.getKey());
ret.add(entry.getValue() ? 1 : 0);
}
return ret;
}
private HashMap<Integer, Boolean> integerArrayToAdapterState(ArrayList<Integer> in) {
HashMap<Integer, Boolean> ret = new HashMap<>();
if (in.size() % 2 != 0) {
throw (new RuntimeException("failed restoring adapterState from integer array list"));
}
for (int i = 0; i < in.size(); i += 2) {
ret.put(in.get(i), in.get(i + 1) == 1);
}
return ret;
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
return super.onCreateOptionsMenu(inputMenu);
}
@Override
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
invalidateOptionsMenu();
return true;
}
return super.onOptionsItemSelected(inputItem);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE, adapterStateToIntegerArray(mAdapter.exportInGroupState()));
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.getText().toString());
}
private void updateLoyaltyCardList() {
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase));
if (mAdapter.getItemCount() == 0) {
mCardList.setVisibility(View.GONE);
noGroupCardsText.setVisibility(View.VISIBLE);
} else {
mCardList.setVisibility(View.VISIBLE);
noGroupCardsText.setVisibility(View.GONE);
}
}
private void leaveWithoutSaving() {
if (hasChanged()) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ManageGroupActivity.this);
builder.setTitle(R.string.leaveWithoutSaveTitle);
builder.setMessage(R.string.leaveWithoutSaveConfirmation);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish());
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
} else {
finish();
}
}
@Override
public boolean onSupportNavigateUp() {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
private boolean hasChanged() {
return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim());
}
@Override
public void onRowLongClicked(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
}
@Override
public void onRowClicked(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
}
}

View File

@@ -1,236 +0,0 @@
package protect.card_locker
import android.content.DialogInterface
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.databinding.ActivityManageGroupBinding
class ManageGroupActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var binding: ActivityManageGroupBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: ManageGroupCursorAdapter
private lateinit var mGroup: Group
private lateinit var mCardList: RecyclerView
private lateinit var noGroupCardsText: TextView
private lateinit var mGroupNameText: EditText
private var mGroupNameNotInUse = false
override fun onCreate(inputSavedInstanceState: Bundle?) {
super.onCreate(inputSavedInstanceState)
binding = ActivityManageGroupBinding.inflate(layoutInflater)
setContentView(binding.root)
Utils.applyWindowInsetsAndFabOffset(binding.root, binding.fabSave)
setSupportActionBar(binding.toolbar)
mDatabase = DBHelper(this).writableDatabase
noGroupCardsText = binding.include.noGroupCardsText
mCardList = binding.include.list
mGroupNameText = binding.editTextGroupName
mGroupNameText.doAfterTextChanged {
mGroupNameNotInUse = true
mGroupNameText.error = null
val currentGroupName = mGroupNameText.text.trim().toString()
if (currentGroupName.isEmpty()) {
mGroupNameText.error = getText(R.string.group_name_is_empty)
return@doAfterTextChanged
}
if (mGroup._id != currentGroupName) {
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
mGroupNameNotInUse = false
mGroupNameText.error = getText(R.string.group_name_already_in_use)
} else {
mGroupNameNotInUse = true
}
}
}
val groupId = intent.getStringExtra("group")
?: throw (IllegalArgumentException("this activity expects a group loaded into it's intent"))
Log.d("groupId", "groupId: $groupId")
mGroup = DBHelper.getGroup(mDatabase, groupId)
?: throw IllegalArgumentException("Cannot load group $groupId from database")
mGroupNameText.setText(mGroup._id)
setTitle(getString(R.string.editGroup, mGroup._id))
mAdapter = ManageGroupCursorAdapter(this, null, this, mGroup, null)
mCardList.adapter = mAdapter
registerForContextMenu(mCardList)
if (inputSavedInstanceState != null) {
mAdapter.importInGroupState(
bundleToAdapterState(
adapterStateBundle = inputSavedInstanceState.getBundle(
SAVE_INSTANCE_ADAPTER_STATE
)
)
)
mGroupNameText.setText(
inputSavedInstanceState.getString(
SAVE_INSTANCE_CURRENT_GROUP_NAME
)
)
}
enableToolbarBackButton()
binding.fabSave.setOnClickListener { v: View ->
val currentGroupName = mGroupNameText.text.trim().toString()
if (currentGroupName != mGroup._id) {
when {
currentGroupName.isEmpty() -> {
Toast.makeText(
applicationContext,
R.string.group_name_is_empty,
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
!mGroupNameNotInUse -> {
Toast.makeText(
applicationContext,
R.string.group_name_already_in_use,
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
}
}
mAdapter.commitToDatabase()
if (currentGroupName != mGroup._id) {
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName)
}
Toast.makeText(
applicationContext,
R.string.group_updated,
Toast.LENGTH_SHORT
).show()
finish()
}
// this setText is here because content_main.xml is reused from main activity
noGroupCardsText.text = getText(R.string.noGiftCardsGroup)
updateLoyaltyCardList()
onBackPressedDispatcher.addCallback(
owner = this,
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
leaveWithoutSaving()
}
})
}
private fun adapterStateToBundle(adapterState: HashMap<Int, Boolean>): Bundle {
val adapterStateBundle = Bundle().apply {
for (entry in adapterState.entries) {
putBoolean(entry.key.toString(), entry.value)
}
}
return adapterStateBundle
}
private fun bundleToAdapterState(adapterStateBundle: Bundle?): Map<Int, Boolean> {
adapterStateBundle ?: return emptyMap()
val adapterStateMap = buildMap {
for (key in adapterStateBundle.keySet()) {
put(key.toInt(), adapterStateBundle.getBoolean(key))
}
}
return adapterStateMap
}
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
menuInflater.inflate(R.menu.card_details_menu, inputMenu)
return super.onCreateOptionsMenu(inputMenu)
}
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
val id = inputItem.itemId
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog()
invalidateOptionsMenu()
return true
}
return super.onOptionsItemSelected(inputItem)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBundle(
SAVE_INSTANCE_ADAPTER_STATE,
adapterStateToBundle(mAdapter.exportInGroupState())
)
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.text.toString())
}
private fun updateLoyaltyCardList() {
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase))
if (mAdapter.itemCount == 0) {
mCardList.visibility = View.GONE
noGroupCardsText.visibility = View.VISIBLE
} else {
mCardList.visibility = View.VISIBLE
noGroupCardsText.visibility = View.GONE
}
}
private fun leaveWithoutSaving() {
if (hasChanged()) {
MaterialAlertDialogBuilder(this@ManageGroupActivity).apply {
setTitle(R.string.leaveWithoutSaveTitle)
setMessage(R.string.leaveWithoutSaveConfirmation)
setPositiveButton(R.string.confirm) { dialog: DialogInterface, _ ->
finish()
}
setNegativeButton(R.string.cancel) { dialog: DialogInterface, _ ->
dialog.dismiss()
}
}.create().show()
} else {
finish()
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressedDispatcher.onBackPressed()
return true
}
private fun hasChanged(): Boolean {
return mAdapter.hasChanged() || mGroup._id != mGroupNameText.text.trim().toString()
}
override fun onRowLongClicked(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
}
override fun onRowClicked(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
}
private companion object {
const val SAVE_INSTANCE_ADAPTER_STATE = "adapterState"
const val SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName"
}
}

View File

@@ -99,7 +99,7 @@ public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
}
}
public void importInGroupState(Map<Integer, Boolean> cardIdInGroupMap) {
public void importInGroupState(HashMap<Integer, Boolean> cardIdInGroupMap) {
mInGroupOverlay = new HashMap<>(cardIdInGroupMap);
}

View File

@@ -0,0 +1,247 @@
package protect.card_locker;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.text.InputType;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List;
import protect.card_locker.databinding.ManageGroupsActivityBinding;
public class ManageGroupsActivity extends CatimaAppCompatActivity implements GroupCursorAdapter.GroupAdapterListener {
private ManageGroupsActivityBinding binding;
private static final String TAG = "Catima";
private SQLiteDatabase mDatabase;
private TextView mHelpText;
private RecyclerView mGroupList;
GroupCursorAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ManageGroupsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.groups);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
mDatabase = new DBHelper(this).getWritableDatabase();
}
@Override
protected void onResume() {
super.onResume();
FloatingActionButton addButton = binding.fabAdd;
addButton.setOnClickListener(v -> createGroup());
addButton.bringToFront();
mGroupList = binding.include.list;
mHelpText = binding.include.helpText;
// Init group list
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
mGroupList.setLayoutManager(mLayoutManager);
mGroupList.setItemAnimator(new DefaultItemAnimator());
mAdapter = new GroupCursorAdapter(this, null, this);
mGroupList.setAdapter(mAdapter);
updateGroupList();
}
private void updateGroupList() {
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase));
if (DBHelper.getGroupCount(mDatabase) == 0) {
mGroupList.setVisibility(View.GONE);
mHelpText.setVisibility(View.VISIBLE);
return;
}
mGroupList.setVisibility(View.VISIBLE);
mHelpText.setVisibility(View.GONE);
}
private void invalidateHomescreenActiveTab() {
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), 0);
activeTabPrefEditor.apply();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
private void createGroup() {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
// Header
builder.setTitle(R.string.enter_group_name);
// Layout
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
params.leftMargin = contentPadding;
params.topMargin = contentPadding / 2;
params.rightMargin = contentPadding;
// EditText with spacing
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setLayoutParams(params);
layout.addView(input);
// Set layout
builder.setView(layout);
// Buttons
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
DBHelper.insertGroup(mDatabase, input.getText().toString().trim());
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
// Now that the dialog exists, we can bind something that affects the OK button
input.addTextChangedListener(new SimpleTextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
String groupName = s.toString().trim();
if (groupName.length() == 0) {
input.setError(getString(R.string.group_name_is_empty));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
return;
}
if (DBHelper.getGroup(mDatabase, groupName) != null) {
input.setError(getString(R.string.group_name_already_in_use));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
return;
}
input.setError(null);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
});
dialog.show();
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
// Set focus on input field
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
private String getGroupName(View view) {
TextView groupNameTextView = view.findViewById(R.id.name);
return (String) groupNameTextView.getText();
}
private void moveGroup(View view, boolean up) {
List<Group> groups = DBHelper.getGroups(mDatabase);
final String groupName = getGroupName(view);
int currentIndex = DBHelper.getGroup(mDatabase, groupName).order;
int newIndex;
// Reinsert group in correct position
if (up) {
newIndex = currentIndex - 1;
} else {
newIndex = currentIndex + 1;
}
// Don't try to move out of bounds
if (newIndex < 0 || newIndex >= groups.size()) {
return;
}
Group group = groups.remove(currentIndex);
groups.add(newIndex, group);
// Update database
DBHelper.reorderGroups(mDatabase, groups);
// Update UI
updateGroupList();
// Ordering may have changed, so invalidate
invalidateHomescreenActiveTab();
}
@Override
public void onMoveDownButtonClicked(View view) {
moveGroup(view, false);
}
@Override
public void onMoveUpButtonClicked(View view) {
moveGroup(view, true);
}
@Override
public void onEditButtonClicked(View view) {
Intent intent = new Intent(this, ManageGroupActivity.class);
intent.putExtra("group", getGroupName(view));
startActivity(intent);
}
@Override
public void onDeleteButtonClicked(View view) {
final String groupName = getGroupName(view);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.deleteConfirmationGroup);
builder.setMessage(groupName);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
DBHelper.deleteGroup(mDatabase, groupName);
updateGroupList();
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
}
}

View File

@@ -1,240 +0,0 @@
package protect.card_locker
import android.content.DialogInterface
import android.content.Intent
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.text.InputType
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.GroupCursorAdapter.GroupAdapterListener
import protect.card_locker.databinding.ManageGroupsActivityBinding
class ManageGroupsActivity : CatimaAppCompatActivity(), GroupAdapterListener {
private lateinit var binding: ManageGroupsActivityBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mHelpText: TextView
private lateinit var mGroupList: RecyclerView
private lateinit var mAdapter: GroupCursorAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ManageGroupsActivityBinding.inflate(layoutInflater)
setTitle(R.string.groups)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
mDatabase = DBHelper(this).writableDatabase
}
override fun onResume() {
super.onResume()
with(binding.fabAdd) {
setOnClickListener { v: View ->
createGroup()
}
bringToFront()
}
mGroupList = binding.include.list
mHelpText = binding.include.helpText
// Init group list
LinearLayoutManager(applicationContext).apply {
mGroupList.layoutManager = this
}
mGroupList.setItemAnimator(DefaultItemAnimator())
mAdapter = GroupCursorAdapter(this, null, this)
mGroupList.setAdapter(mAdapter)
updateGroupList()
}
private fun updateGroupList() {
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase))
if (DBHelper.getGroupCount(mDatabase) == 0) {
mGroupList.visibility = View.GONE
mHelpText.visibility = View.VISIBLE
return
}
mGroupList.visibility = View.VISIBLE
mHelpText.visibility = View.GONE
}
private fun invalidateHomescreenActiveTab() {
val activeTabPref = getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
)
activeTabPref.edit {
putInt(getString(R.string.sharedpreference_active_tab), 0)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
}
return super.onOptionsItemSelected(item)
}
private fun createGroup() {
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this)
// Header
builder.setTitle(R.string.enter_group_name)
// Layout
val layout = LinearLayout(this)
layout.orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
val contentPadding =
resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
leftMargin = contentPadding
topMargin = contentPadding / 2
rightMargin = contentPadding
}
// EditText with spacing
val input = EditText(this)
input.setInputType(InputType.TYPE_CLASS_TEXT)
input.setLayoutParams(params)
layout.addView(input)
// Set layout
builder.setView(layout)
// Buttons
builder.setPositiveButton(getString(R.string.ok)) { dialog: DialogInterface, which: Int ->
DBHelper.insertGroup(mDatabase, input.text.trim().toString())
updateGroupList()
}
builder.setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
val dialog = builder.create()
// Now that the dialog exists, we can bind something that affects the OK button
input.doOnTextChanged { s: CharSequence?, start: Int, before: Int, count: Int ->
val groupName = s?.trim().toString()
if (groupName.isEmpty()) {
input.error = getString(R.string.group_name_is_empty)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
return@doOnTextChanged
}
if (DBHelper.getGroup(mDatabase, groupName) != null) {
input.error = getString(R.string.group_name_already_in_use)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
return@doOnTextChanged
}
input.error = null
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true)
}
dialog.apply {
show()
// Disable button (must be done **after** dialog is shown to prevent crash
getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
// Set focus on input field
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
input.requestFocus()
}
private fun getGroupName(view: View): String {
val groupNameTextView = view.findViewById<TextView>(R.id.name)
return groupNameTextView.text.toString()
}
private fun moveGroup(view: View, up: Boolean) {
val groups = DBHelper.getGroups(mDatabase)
val groupName = getGroupName(view)
val currentIndex = DBHelper.getGroup(mDatabase, groupName).order
// Reinsert group in correct position
val newIndex: Int = if (up) {
currentIndex - 1
} else {
currentIndex + 1
}
// Don't try to move out of bounds
if (newIndex < 0 || newIndex >= groups.size) {
return
}
val group = groups.removeAt(currentIndex)
groups.add(newIndex, group)
// Update database
DBHelper.reorderGroups(mDatabase, groups)
// Update UI
updateGroupList()
// Ordering may have changed, so invalidate
invalidateHomescreenActiveTab()
}
override fun onMoveDownButtonClicked(view: View) {
moveGroup(view, false)
}
override fun onMoveUpButtonClicked(view: View) {
moveGroup(view, true)
}
override fun onEditButtonClicked(view: View) {
Intent(this, ManageGroupActivity::class.java).apply {
putExtra("group", getGroupName(view))
startActivity(this)
}
}
override fun onDeleteButtonClicked(view: View) {
val groupName = getGroupName(view)
MaterialAlertDialogBuilder(this).apply {
setTitle(R.string.deleteConfirmationGroup)
setMessage(groupName)
setPositiveButton(getString(R.string.ok)) { dialog: DialogInterface, which: Int ->
DBHelper.deleteGroup(mDatabase, groupName)
updateGroupList()
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab()
}
setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
}.create().show()
}
}

View File

@@ -1,73 +0,0 @@
package protect.card_locker
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.model.LocalFileHeader
import java.io.FileNotFoundException
import java.io.IOException
class PkpassesParser(context: Context, uri: Uri?) {
private var mContext = context
private val pkPassParsers: ArrayList<PkpassParser> = ArrayList()
init {
mContext = context
Log.i(TAG, "Received Pkpasses file")
if (uri == null) {
Log.e(TAG, "Uri did not contain any data")
throw IOException(context.getString(R.string.errorReadingFile))
}
try {
mContext.contentResolver.openInputStream(uri).use { inputStream ->
ZipInputStream(inputStream).use { zipInputStream ->
var localFileHeader: LocalFileHeader?
while (true) {
// Retrieve the next file
localFileHeader = zipInputStream.nextEntry
// If no next file, exit loop
if (localFileHeader == null) {
break
}
// Ignore directories
if (localFileHeader.isDirectory) continue
// Ignore non-pkpass files
if (!localFileHeader.fileName.endsWith(".pkpass")) continue
// Extract .pkpass (.zip) inside .pkpasses to cache directory
val tempFileName = "pkpassparser_" + System.currentTimeMillis() + "_" + localFileHeader.fileName
val tempFile = Utils.copyToTempFile(mContext, zipInputStream, tempFileName)
// Parse temporary file
pkPassParsers.add(
PkpassParser(mContext, tempFile.toUri())
)
// Delete temporary file
tempFile.delete()
}
}
}
} catch (e: FileNotFoundException) {
throw IOException(mContext.getString(R.string.errorReadingFile))
} catch (e: Exception) {
throw e
}
}
fun getPkpassParsers(): ArrayList<PkpassParser> {
return pkPassParsers
}
companion object {
private const val TAG = "Catima"
}
}

View File

@@ -0,0 +1,542 @@
package protect.card_locker;
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_CONTENTS;
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_FORMAT;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.zxing.DecodeHintType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.android.Intents;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import protect.card_locker.databinding.CustomBarcodeScannerBinding;
import protect.card_locker.databinding.ScanActivityBinding;
/**
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
* <p>
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
* originally licensed under Apache 2.0
*/
public class ScanActivity extends CatimaAppCompatActivity {
private ScanActivityBinding binding;
private CustomBarcodeScannerBinding customBarcodeScannerBinding;
private static final String TAG = "Catima";
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
private static final int COMPAT_SCALE_FACTOR_DIP = 320;
private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100;
private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101;
private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102;
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
private String cardId;
private String addGroup;
private boolean torch = false;
private ActivityResultLauncher<Intent> manualAddLauncher;
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
private ActivityResultLauncher<Intent> photoPickerLauncher;
private ActivityResultLauncher<Intent> pdfPickerLauncher;
private ActivityResultLauncher<Intent> pkpassPickerLauncher;
static final String STATE_SCANNER_ACTIVE = "scannerActive";
private boolean mScannerActive = true;
private boolean mHasError = false;
private void extractIntentFields(Intent intent) {
final Bundle b = intent.getExtras();
cardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
Log.d(TAG, "Scan activity: id=" + cardId);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ScanActivityBinding.inflate(getLayoutInflater());
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner);
setTitle(R.string.scanCardBarcode);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
extractIntentFields(getIntent());
manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData()));
photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData()));
pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData()));
pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData()));
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
setScannerActive(false);
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
String[] texts = new String[]{
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile),
getString(R.string.addFromPkpass)
};
Object[] icons = new Object[]{
R.drawable.baseline_block_24,
R.drawable.ic_edit,
R.drawable.baseline_image_24,
R.drawable.baseline_picture_as_pdf_24,
R.drawable.local_activity_24px
};
String[] columns = new String[]{"text", "icon"};
for (int i = 0; i < texts.length; i++) {
HashMap<String, Object> map = new HashMap<>();
map.put(columns[0], texts[i]);
map.put(columns[1], icons[i]);
list.add(map);
}
ListAdapter adapter = new SimpleAdapter(
ScanActivity.this,
list,
R.layout.alertdialog_row_with_icon,
columns,
new int[]{R.id.textView, R.id.imageView}
);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
builder.setAdapter(
adapter,
(dialogInterface, i) -> {
switch (i) {
case 0:
addWithoutBarcode();
break;
case 1:
addManually();
break;
case 2:
addFromImage();
break;
case 3:
addFromPdf();
break;
case 4:
addFromPkPass();
break;
default:
throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
}
}
);
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
builder.show();
});
// Configure barcodeScanner
barcodeScannerView = binding.zxingBarcodeScanner;
Intent barcodeScannerIntent = new Intent();
Bundle barcodeScannerIntentBundle = new Bundle();
barcodeScannerIntentBundle.putBoolean(DecodeHintType.ALSO_INVERTED.name(), Boolean.TRUE);
barcodeScannerIntent.putExtras(barcodeScannerIntentBundle);
barcodeScannerView.initializeFromIntent(barcodeScannerIntent);
// Even though we do the actual decoding with the barcodeScannerView
// CaptureManager needs to be running to show the camera and scanning bar
capture = new CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError);
Intent captureIntent = new Intent();
Bundle captureIntentBundle = new Bundle();
captureIntentBundle.putBoolean(Intents.Scan.BEEP_ENABLED, false);
captureIntent.putExtras(captureIntentBundle);
capture.initializeFromIntent(captureIntent, savedInstanceState);
barcodeScannerView.decodeSingle(new BarcodeCallback() {
@Override
public void barcodeResult(BarcodeResult result) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(result.getText());
loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat()));
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
}
@Override
public void possibleResultPoints(List<ResultPoint> resultPoints) {
}
});
}
@Override
protected void onResume() {
super.onResume();
if (mScannerActive) {
capture.onResume();
}
if (!Utils.deviceHasCamera(this)) {
showCameraError(getString(R.string.noCameraFoundGuideText), false);
} else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
showCameraPermissionMissingText();
} else {
hideCameraError();
}
scaleScreen();
}
@Override
protected void onPause() {
super.onPause();
capture.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
capture.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
capture.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive);
}
@Override
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
getMenuInflater().inflate(R.menu.scan_menu, menu);
}
barcodeScannerView.setTorchOff();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
setResult(Activity.RESULT_CANCELED);
finish();
return true;
} else if (item.getItemId() == R.id.action_toggle_flashlight) {
if (torch) {
torch = false;
barcodeScannerView.setTorchOff();
item.setTitle(R.string.turn_flashlight_on);
item.setIcon(R.drawable.ic_flashlight_off_white_24dp);
} else {
torch = true;
barcodeScannerView.setTorchOn();
item.setTitle(R.string.turn_flashlight_off);
item.setIcon(R.drawable.ic_flashlight_on_white_24dp);
}
}
return super.onOptionsItemSelected(item);
}
private void setScannerActive(boolean isActive) {
if (isActive) {
barcodeScannerView.resume();
} else {
barcodeScannerView.pause();
}
mScannerActive = isActive;
}
private void returnResult(ParseResult parseResult) {
Intent result = new Intent();
Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this);
if (addGroup != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
result.putExtras(bundle);
ScanActivity.this.setResult(RESULT_OK, result);
finish();
}
private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
List<ParseResult> parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
if (parseResultList.isEmpty()) {
setScannerActive(true);
return;
}
Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
returnResult(parseResult);
}
@Override
public void onUserDismissedSelector() {
setScannerActive(true);
}
});
}
private void addWithoutBarcode() {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
// Header
builder.setTitle(R.string.addWithoutBarcode);
// Layout
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
params.leftMargin = contentPadding;
params.topMargin = contentPadding / 2;
params.rightMargin = contentPadding;
// Description
TextView currentTextview = new TextView(this);
currentTextview.setText(getString(R.string.enter_card_id));
currentTextview.setLayoutParams(params);
layout.addView(currentTextview);
// EditText with spacing
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setLayoutParams(params);
layout.addView(input);
// Set layout
builder.setView(layout);
// Buttons
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(input.getText().toString());
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
// Now that the dialog exists, we can bind something that affects the OK button
input.addTextChangedListener(new SimpleTextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s.length() == 0) {
input.setError(getString(R.string.card_id_must_not_be_empty));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} else {
input.setError(null);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
}
});
dialog.show();
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
// Set focus on input field
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
public void addManually() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
builder.setTitle(R.string.add_manually_warning_title);
builder.setMessage(R.string.add_manually_warning_message);
builder.setPositiveButton(R.string.continue_, (dialog, which) -> {
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
if (cardId != null) {
final Bundle b = new Bundle();
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
i.putExtras(b);
}
manualAddLauncher.launch(i);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> setScannerActive(true));
builder.setOnCancelListener(dialog -> setScannerActive(true));
builder.show();
}
public void addFromImage() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
}
public void addFromPdf() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF);
}
public void addFromPkPass() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS);
}
private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher<Intent> launcher, int chooserText, int errorMessage) {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType(mimeType);
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentIntent.setType(mimeType);
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
try {
launcher.launch(chooserIntent);
} catch (ActivityNotFoundException e) {
setScannerActive(true);
Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
}
public void onCaptureManagerError(String errorMessage) {
if (mHasError) {
// We're already showing an error, ignore this new error
return;
}
showCameraError(errorMessage, false);
}
private void showCameraPermissionMissingText() {
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true);
}
private void showCameraError(String message, boolean setOnClick) {
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.setText(message);
setCameraErrorState(true, setOnClick);
}
private void hideCameraError() {
setCameraErrorState(false, false);
}
private void setCameraErrorState(boolean visible, boolean setOnClick) {
mHasError = visible;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(visible && setOnClick ? v -> {
navigateToSystemPermissionSetting();
} : null);
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(visible ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
customBarcodeScannerBinding.cameraErrorLayout.getRoot().setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void scaleScreen() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
}
private int obtainThemeAttribute(int attribute) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(attribute, typedValue, true);
return typedValue.data;
}
private void navigateToSystemPermissionSetting() {
Intent permissionIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getPackageName(), null));
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(permissionIntent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
if (granted) {
hideCameraError();
} else {
showCameraPermissionMissingText();
}
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) {
if (granted) {
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker);
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
} else {
addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager);
}
} else {
setScannerActive(true);
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show();
}
}
}
}

View File

@@ -1,599 +0,0 @@
package protect.card_locker
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.text.InputType
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ListAdapter
import android.widget.SimpleAdapter
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.zxing.DecodeHintType
import com.google.zxing.ResultPoint
import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.CaptureManager
import com.journeyapps.barcodescanner.DecoratedBarcodeView
import protect.card_locker.databinding.CustomBarcodeScannerBinding
import protect.card_locker.databinding.ScanActivityBinding
/**
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
* <p>
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
* originally licensed under Apache 2.0
*/
class ScanActivity : CatimaAppCompatActivity() {
private lateinit var binding: ScanActivityBinding
private lateinit var customBarcodeScannerBinding: CustomBarcodeScannerBinding
companion object {
private const val TAG = "Catima"
private const val MEDIUM_SCALE_FACTOR_DIP = 460
private const val COMPAT_SCALE_FACTOR_DIP = 320
private const val PERMISSION_SCAN_ADD_FROM_IMAGE = 100
private const val PERMISSION_SCAN_ADD_FROM_PDF = 101
private const val PERMISSION_SCAN_ADD_FROM_PKPASS = 102
private const val STATE_SCANNER_ACTIVE = "scannerActive"
}
private lateinit var capture: CaptureManager
private lateinit var barcodeScannerView: DecoratedBarcodeView
private var cardId: String? = null
private var addGroup: String? = null
private var torch = false
private lateinit var manualAddLauncher: ActivityResultLauncher<Intent>
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
private lateinit var photoPickerLauncher: ActivityResultLauncher<Intent>
private lateinit var pdfPickerLauncher: ActivityResultLauncher<Intent>
private lateinit var pkpassPickerLauncher: ActivityResultLauncher<Intent>
private var mScannerActive = true
private var mHasError = false
private fun extractIntentFields(intent: Intent) {
val b = intent.extras
cardId = b?.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID)
addGroup = b?.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)
Log.d(TAG, "Scan activity: id=$cardId")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ScanActivityBinding.inflate(layoutInflater)
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner)
setTitle(R.string.scanCardBarcode)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
extractIntentFields(intent)
manualAddLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.SELECT_BARCODE_REQUEST,
result.resultCode,
result.data
)
}
photoPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_IMAGE_FILE,
result.resultCode,
result.data
)
}
pdfPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_PDF_FILE,
result.resultCode,
result.data
)
}
pkpassPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_PKPASS_FILE,
result.resultCode,
result.data
)
}
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener {
setScannerActive(false)
val list: ArrayList<HashMap<String, Any>> = arrayListOf()
val texts = arrayOf(
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile),
getString(R.string.addFromPkpass)
)
val icons = arrayOf(
R.drawable.baseline_block_24,
R.drawable.ic_edit,
R.drawable.baseline_image_24,
R.drawable.baseline_picture_as_pdf_24,
R.drawable.local_activity_24px
)
val columns = arrayOf("text", "icon")
for (i in 0 until texts.size) {
val map: HashMap<String, Any> = hashMapOf()
map.put(columns[0], texts[i])
map.put(columns[1], icons[i])
list.add(map)
}
val adapter: ListAdapter = SimpleAdapter(
this,
list,
R.layout.alertdialog_row_with_icon,
columns,
intArrayOf(R.id.textView, R.id.imageView)
)
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(getString(R.string.add_a_card_in_a_different_way))
setAdapter(adapter) { _, i ->
when (i) {
0 -> addWithoutBarcode()
1 -> addManually()
2 -> addFromImage()
3 -> addFromPdf()
4 -> addFromPkPass()
else -> throw IllegalArgumentException(
"Unknown 'Add a card in a different way' dialog option: $i"
)
}
}
setOnCancelListener { _ -> setScannerActive(true) }
}
builder.show()
}
// Configure barcodeScanner
barcodeScannerView = binding.zxingBarcodeScanner
val barcodeScannerIntent = Intent().apply {
val barcodeScannerIntentBundle = Bundle().apply {
putBoolean(DecodeHintType.ALSO_INVERTED.name, true)
}
putExtras(barcodeScannerIntentBundle)
}
barcodeScannerView.initializeFromIntent(barcodeScannerIntent)
// Even though we do the actual decoding with the barcodeScannerView
// CaptureManager needs to be running to show the camera and scanning bar
capture = CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError)
val captureIntent = Intent().apply {
val captureIntentBundle = Bundle().apply {
putBoolean(DecodeHintType.ALSO_INVERTED.name, false)
}
putExtras(captureIntentBundle)
}
capture.initializeFromIntent(captureIntent, savedInstanceState)
barcodeScannerView.decodeSingle(object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
val loyaltyCard = LoyaltyCard().apply {
setCardId(result.text)
setBarcodeType(CatimaBarcode.fromBarcode(result.barcodeFormat))
}
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
}
override fun possibleResultPoints(resultPoints: List<ResultPoint?>?) {}
})
}
override fun onResume() {
super.onResume()
if (mScannerActive) {
capture.onResume()
}
if (!Utils.deviceHasCamera(this)) {
showCameraError(getString(R.string.noCameraFoundGuideText), false)
} else if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
showCameraPermissionMissingText()
} else {
hideCameraError()
}
scaleScreen()
}
override fun onPause() {
super.onPause()
capture.onPause()
}
override fun onDestroy() {
super.onDestroy()
capture.onDestroy()
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
capture.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
menuInflater.inflate(R.menu.scan_menu, menu)
}
barcodeScannerView.setTorchOff()
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
setResult(RESULT_CANCELED)
finish()
return true
} else if (item.itemId == R.id.action_toggle_flashlight) {
if (torch) {
torch = false
barcodeScannerView.setTorchOff()
item.setTitle(R.string.turn_flashlight_on)
item.setIcon(R.drawable.ic_flashlight_off_white_24dp)
} else {
torch = true
barcodeScannerView.setTorchOn()
item.setTitle(R.string.turn_flashlight_off)
item.setIcon(R.drawable.ic_flashlight_on_white_24dp)
}
}
return super.onOptionsItemSelected(item)
}
private fun setScannerActive(isActive: Boolean) {
if (isActive) {
barcodeScannerView.resume()
} else {
barcodeScannerView.pause()
}
mScannerActive = isActive
}
private fun returnResult(parseResult: ParseResult) {
val bundle = parseResult.toLoyaltyCardBundle(this).apply {
addGroup?.let { putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, it) }
}
val result = Intent().apply { putExtras(bundle) }
this.setResult(RESULT_OK, result)
finish()
}
private fun handleActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(resultCode, resultCode, intent)
val parseResultList: List<ParseResult> =
Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this)
if (parseResultList.isEmpty()) {
setScannerActive(true)
return
}
Utils.makeUserChooseParseResultFromList(
this,
parseResultList,
object : ParseResultListDisambiguatorCallback {
override fun onUserChoseParseResult(parseResult: ParseResult) {
returnResult(parseResult)
}
override fun onUserDismissedSelector() {
setScannerActive(true)
}
})
}
private fun addWithoutBarcode() {
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this).apply {
setOnCancelListener { dialogInterface -> setScannerActive(true) }
// Header
setTitle(R.string.addWithoutBarcode)
}
// Layout
val layout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
val contentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
leftMargin = contentPadding
topMargin = contentPadding / 2
rightMargin = contentPadding
}
// Description
val currentTextview = TextView(this).apply {
text = getString(R.string.enter_card_id)
layoutParams = params
}
layout.addView(currentTextview)
//EditText with spacing
val input = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT
layoutParams = params
}
layout.addView(input)
// Set layout
builder.setView(layout).apply {
setPositiveButton(getString(R.string.ok)) { _, _ ->
val loyaltyCard = LoyaltyCard()
loyaltyCard.cardId = input.text.toString()
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
}
setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
}
val dialog: AlertDialog = builder.create()
// Now that the dialog exists, we can bind something that affects the OK button
input.doOnTextChanged { text, _, _, _ ->
if (text.isNullOrEmpty()) {
input.error = getString(R.string.card_id_must_not_be_empty)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
} else {
input.error = null
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
}
}
dialog.show()
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
// Set focus on input field
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
input.requestFocus()
}
fun addManually() {
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(R.string.add_manually_warning_title)
setMessage(R.string.add_manually_warning_message)
setPositiveButton(R.string.continue_) { _, _ ->
val i = Intent(applicationContext, BarcodeSelectorActivity::class.java)
if (cardId != null) {
val b = Bundle()
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId)
i.putExtras(b)
}
manualAddLauncher.launch(i)
}
setNegativeButton(R.string.cancel) { _, _ -> setScannerActive(true) }
setOnCancelListener { _ -> setScannerActive(true) }
}
builder.show()
}
fun addFromImage() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE)
}
fun addFromPdf() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF)
}
fun addFromPkPass() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS)
}
private fun addFromImageOrFileAfterPermission(
mimeType: String,
launcher: ActivityResultLauncher<Intent>,
chooserText: Int,
errorMessage: Int
) {
val photoPickerIntent = Intent(Intent.ACTION_PICK)
photoPickerIntent.type = mimeType
val contentIntent = Intent(Intent.ACTION_GET_CONTENT)
contentIntent.type = mimeType
val chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText))
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(contentIntent))
try {
launcher.launch(chooserIntent)
} catch (e: ActivityNotFoundException) {
setScannerActive(true)
Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
fun onCaptureManagerError(errorMessage: String) {
if (mHasError) {
// We're already showing an error, ignore this new error
return
}
showCameraError(errorMessage, false)
}
private fun showCameraPermissionMissingText() {
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true)
}
private fun showCameraError(message: String, setOnClick: Boolean) {
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.text = message
setCameraErrorState(true, setOnClick)
}
private fun hideCameraError() {
setCameraErrorState(false, false)
}
private fun setCameraErrorState(visible: Boolean, setOnClick: Boolean) {
mHasError = visible
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(
if (visible && setOnClick) { _ -> navigateToSystemPermissionSetting() }
else null
)
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(
if (visible) obtainThemeAttribute(com.google.android.material.R.attr.colorSurface)
else Color.TRANSPARENT
)
customBarcodeScannerBinding.cameraErrorLayout.root.visibility =
if (visible) View.VISIBLE else View.GONE
}
private fun scaleScreen() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val screenHeight: Int = displayMetrics.heightPixels
val mediumSizePx: Float = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
resources.displayMetrics
)
val shouldScaleSmaller = screenHeight < mediumSizePx
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.visibility =
if (shouldScaleSmaller) View.GONE else View.VISIBLE
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.visibility =
if (shouldScaleSmaller) View.GONE else View.VISIBLE
}
private fun obtainThemeAttribute(attribute: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.data
}
private fun navigateToSystemPermissionSetting() {
val permissionIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", getPackageName(), null)
)
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(permissionIntent)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
onMockedRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onMockedRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
val granted =
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
if (granted) {
hideCameraError()
} else {
showCameraPermissionMissingText()
}
} else if (requestCode in listOf(
PERMISSION_SCAN_ADD_FROM_IMAGE,
PERMISSION_SCAN_ADD_FROM_PDF,
PERMISSION_SCAN_ADD_FROM_PKPASS
)
) {
if (granted) {
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
addFromImageOrFileAfterPermission(
"image/*",
photoPickerLauncher,
R.string.addFromImage,
R.string.failedLaunchingPhotoPicker
)
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
addFromImageOrFileAfterPermission(
"application/pdf",
pdfPickerLauncher,
R.string.addFromPdfFile,
R.string.failedLaunchingFileManager
)
} else {
addFromImageOrFileAfterPermission(
"application/*",
pkpassPickerLauncher,
R.string.addFromPkpass,
R.string.failedLaunchingFileManager
)
}
} else {
setScannerActive(true)
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG)
.show()
}
}
}
}

View File

@@ -0,0 +1,29 @@
package protect.card_locker;
public class ThirdPartyInfo {
private final String mName;
private final String mUrl;
private final String mLicense;
public ThirdPartyInfo(String name, String url, String license) {
mName = name;
mUrl = url;
mLicense = license;
}
public String name() {
return mName;
}
public String url() {
return mUrl;
}
public String license() {
return mLicense;
}
public String toHtml() {
return String.format("<a href=\"%s\">%s</a> (%s)", url(), name(), license());
}
}

View File

@@ -1,23 +0,0 @@
package protect.card_locker
class ThirdPartyInfo(
private val mName: String,
private val mUrl: String,
private val mLicense: String
) {
fun name(): String {
return mName
}
fun url(): String {
return mUrl
}
fun license(): String {
return mLicense
}
fun toHtml(): String {
return String.format("<a href=\"%s\">%s</a> (%s)", url(), name(), license())
}
}

View File

@@ -0,0 +1,93 @@
package protect.card_locker;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.textview.MaterialTextView;
import com.yalantis.ucrop.UCropActivity;
public class UCropWrapper extends UCropActivity {
public static final String UCROP_TOOLBAR_TYPEFACE_STYLE = "ucop_toolbar_typeface_style";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utils.applyWindowInsets(findViewById(android.R.id.content));
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
boolean darkMode = Utils.isDarkModeEnabled(this);
Window window = getWindow();
// setup status bar to look like the rest of the app
if (Build.VERSION.SDK_INT >= 23) {
if (window != null) {
View decorView = window.getDecorView();
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
wic.setAppearanceLightStatusBars(!darkMode);
}
} else {
// icons are always white back then
if (window != null && !darkMode) {
window.setStatusBarColor(ColorUtils.compositeColors(Color.argb(127, 0, 0, 0), window.getStatusBarColor()));
}
}
// find and check views that we wish to color modify
// for when we update ucrop or switch to another cropper
View check = findViewById(com.yalantis.ucrop.R.id.wrapper_controls);
if (check instanceof FrameLayout) {
FrameLayout controls = (FrameLayout) check;
check = findViewById(com.yalantis.ucrop.R.id.wrapper_states);
if (check instanceof LinearLayout) {
LinearLayout states = (LinearLayout) check;
for (int i = 0; i < controls.getChildCount(); i++) {
check = controls.getChildAt(i);
if (check instanceof AppCompatImageView) {
AppCompatImageView controlsBackgroundImage = (AppCompatImageView) check;
// everything gathered and are as expected, now perform color patching
Utils.patchColors(this);
int colorSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
Drawable controlsBackgroundImageDrawable = controlsBackgroundImage.getBackground();
controlsBackgroundImageDrawable.mutate();
controlsBackgroundImageDrawable.setTint(darkMode ? colorOnSurface : colorSurface);
controlsBackgroundImage.setBackgroundDrawable(controlsBackgroundImageDrawable);
states.setBackgroundColor(darkMode ? colorSurface : colorOnSurface);
break;
}
}
}
}
// change toolbar font
check = findViewById(com.yalantis.ucrop.R.id.toolbar_title);
if (check instanceof MaterialTextView) {
MaterialTextView toolbarTextview = (MaterialTextView) check;
Intent intent = getIntent();
int style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1);
if (style != -1) {
toolbarTextview.setTypeface(Typeface.defaultFromStyle(style));
}
}
}
}

View File

@@ -1,122 +0,0 @@
package protect.card_locker
import android.graphics.Color
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.children
import com.google.android.material.color.MaterialColors
import com.google.android.material.textview.MaterialTextView
import com.yalantis.ucrop.UCropActivity
class UCropWrapper : UCropActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Utils.applyWindowInsets(findViewById(android.R.id.content))
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
val darkMode = Utils.isDarkModeEnabled(this)
// setup status bar to look like the rest of the app
setupStatusBar(darkMode)
// find and check views that we wish to color modify
// for when we update ucrop or switch to another cropper
checkViews(darkMode)
// change toolbar font
changeToolbarFont()
}
private fun setupStatusBar(darkMode: Boolean) {
if (window == null) {
return
}
if (Build.VERSION.SDK_INT >= 23) {
val decorView = window.decorView
val wic = WindowInsetsControllerCompat(window, decorView)
wic.isAppearanceLightStatusBars = !darkMode
} else if (!darkMode) {
window.statusBarColor = ColorUtils.compositeColors(
Color.argb(127, 0, 0, 0),
window.statusBarColor
)
}
}
private fun checkViews(darkMode: Boolean) {
var view = findViewById<View?>(com.yalantis.ucrop.R.id.wrapper_controls)
if (view !is FrameLayout) {
return
}
val controls = view
view = findViewById(com.yalantis.ucrop.R.id.wrapper_states)
if (view !is LinearLayout) {
return
}
val states = view
controls.children.firstOrNull { it is AppCompatImageView }?.let {
// everything gathered and are as expected, now perform color patching
Utils.patchColors(this)
val colorSurface = MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurface,
ContextCompat.getColor(
this,
R.color.md_theme_light_surface
)
)
val colorOnSurface = MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorOnSurface,
ContextCompat.getColor(
this,
R.color.md_theme_light_onSurface
)
)
val controlsBackgroundImageDrawable = it.background
controlsBackgroundImageDrawable.mutate()
controlsBackgroundImageDrawable.setTint(
if (darkMode) {
colorOnSurface
} else {
colorSurface
}
)
it.background = controlsBackgroundImageDrawable
states.setBackgroundColor(
if (darkMode) {
colorSurface
} else {
colorOnSurface
}
)
}
}
private fun changeToolbarFont() {
val toolbar = findViewById<View?>(com.yalantis.ucrop.R.id.toolbar_title)
if (toolbar !is MaterialTextView) {
return
}
val style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1)
if (style != -1) {
toolbar.setTypeface(Typeface.defaultFromStyle(style))
}
}
internal companion object {
const val UCROP_TOOLBAR_TYPEFACE_STYLE: String = "ucop_toolbar_typeface_style"
}
}

View File

@@ -87,10 +87,10 @@ import java.util.Currency;
import java.util.Date;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -228,58 +228,6 @@ public class Utils {
return parseResultList;
}
static public List<ParseResult> retrieveBarcodesFromPkPasses(Context context, Uri uri) {
Log.i(TAG, "Received Pkpasses file with possible barcode");
if (uri == null) {
Log.e(TAG, "Pkpasses did not contain any data");
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
PkpassesParser pkpassesParser;
try {
pkpassesParser = new PkpassesParser(context, uri);
} catch (Exception e) {
Log.e(TAG, "Error reading pkpasses file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
List<ParseResult> parseResultList = new ArrayList<>();
int i = 0;
for (PkpassParser pkpassParser : pkpassesParser.getPkpassParsers()) {
ParseResult parseResult;
List<String> locales = pkpassParser.listLocales();
if (locales.isEmpty()) {
try {
parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(null));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
parseResult.setNote(String.format(context.getString(R.string.cardWithNumber), i+1));
parseResultList.add(parseResult);
} else {
for (String locale : locales) {
try {
parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(locale));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
parseResult.setNote(String.format(context.getString(R.string.cardWithNumberAndLocale), i+1, locale));
parseResultList.add(parseResult);
}
}
i++;
}
return parseResultList;
}
static public List<ParseResult> retrieveBarcodesFromPdf(Context context, Uri uri) {
Log.i(TAG, "Received PDF file with possible barcode");
if (uri == null) {
@@ -371,19 +319,7 @@ public class Utils {
}
if (requestCode == Utils.BARCODE_IMPORT_FROM_PKPASS_FILE) {
Uri intentData = intent.getData();
if (intentData == null) {
Log.e(TAG, "Uri did not contain any data");
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
if (Objects.equals(context.getContentResolver().getType(intentData), "application/vnd.apple.pkpasses")) {
return retrieveBarcodesFromPkPasses(context, intentData);
}
return retrieveBarcodesFromPkPass(context, intentData);
return retrieveBarcodesFromPkPass(context, intent.getData());
}
if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) {
@@ -914,7 +850,7 @@ public class Utils {
public static File copyToTempFile(Context context, InputStream input, String name) throws IOException {
File file = createTempFile(context, name);
try (FileOutputStream out = new FileOutputStream(file)) {
try (input; FileOutputStream out = new FileOutputStream(file)) {
byte[] buf = new byte[4096];
int len;
while ((len = input.read(buf)) != -1) {

View File

@@ -0,0 +1,9 @@
package protect.card_locker.async;
import java.util.concurrent.Callable;
public interface CompatCallable<T> extends Callable<T> {
void onPostExecute(Object result);
void onPreExecute();
}

View File

@@ -1,9 +0,0 @@
package protect.card_locker.async
import java.util.concurrent.Callable
interface CompatCallable<T> : Callable<T?> {
fun onPostExecute(result: Any?)
fun onPreExecute()
}

View File

@@ -0,0 +1,8 @@
package protect.card_locker.importexport;
public enum DataFormat {
Catima,
Fidme,
Stocard,
VoucherVault;
}

View File

@@ -1,7 +0,0 @@
package protect.card_locker.importexport
enum class DataFormat {
Catima,
Fidme,
VoucherVault
}

View File

@@ -0,0 +1,20 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import java.io.IOException;
import java.io.OutputStream;
/**
* Interface for a class which can export the contents of the database
* in a given format.
*/
public interface Exporter {
/**
* Export the database to the output stream in a given format.
*
* @throws IOException
*/
void exportData(Context context, SQLiteDatabase database, OutputStream output, char[] password) throws IOException, InterruptedException;
}

View File

@@ -1,25 +0,0 @@
package protect.card_locker.importexport
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import java.io.IOException
import java.io.OutputStream
/**
* Interface for a class which can export the contents of the database
* in a given format.
*/
interface Exporter {
/**
* Export the database to the output stream in a given format.
*
* @throws IOException, InterruptedException
*/
@Throws(IOException::class, InterruptedException::class)
fun exportData(
context: Context,
database: SQLiteDatabase,
output: OutputStream,
password: CharArray
)
}

View File

@@ -0,0 +1,7 @@
package protect.card_locker.importexport;
public enum ImportExportResultType {
Success,
GenericFailure,
BadPassword;
}

View File

@@ -1,7 +0,0 @@
package protect.card_locker.importexport
enum class ImportExportResultType {
Success,
GenericFailure,
BadPassword
}

View File

@@ -0,0 +1,27 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import protect.card_locker.FormatException;
/**
* Interface for a class which can import the contents of a stream
* into the database.
*/
public interface Importer {
/**
* Import data from the input stream in a given format into
* the database.
*
* @throws IOException
* @throws FormatException
*/
void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
}

View File

@@ -1,39 +0,0 @@
package protect.card_locker.importexport
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import org.json.JSONException
import protect.card_locker.FormatException
import java.io.File
import java.io.IOException
import java.text.ParseException
/**
* Interface for a class which can import the contents of a stream
* into the database.
*/
interface Importer {
/**
* Import data from the input stream in a given format into
* the database.
*
* @throws IOException
* @throws FormatException
* @throws InterruptedException
* @throws JSONException
* @throws ParseException
*/
@Throws(
IOException::class,
FormatException::class,
InterruptedException::class,
JSONException::class,
ParseException::class
)
fun importData(
context: Context,
database: SQLiteDatabase,
inputFile: File,
password: CharArray
)
}

View File

@@ -37,6 +37,9 @@ public class MultiFormatImporter {
case Fidme:
importer = new FidmeImporter();
break;
case Stocard:
importer = new StocardImporter();
break;
case VoucherVault:
importer = new VoucherVaultImporter();
break;

View File

@@ -0,0 +1,428 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import protect.card_locker.CatimaBarcode;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
import protect.card_locker.ImageLocationType;
import protect.card_locker.LoyaltyCard;
import protect.card_locker.R;
import protect.card_locker.Utils;
import protect.card_locker.ZipUtils;
/**
* Class for importing a database from CSV (Comma Separate Values)
* formatted data.
* <p>
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class StocardImporter implements Importer {
public static class StocardProvider {
public String name = null;
public String barcodeFormat = null;
public Bitmap logo = null;
}
public static class StocardRecord {
public String providerId = null;
public String store = null;
public String label = null;
public String note = null;
public String cardId = null;
public String barcodeType = null;
public Long lastUsed = null;
public Bitmap frontImage = null;
public Bitmap backImage = null;
@NonNull
@Override
public String toString() {
return String.format(
"StocardRecord{%n providerId=%s,%n store=%s,%n label=%s,%n note=%s,%n cardId=%s,%n"
+ " barcodeType=%s,%n lastUsed=%s,%n frontImage=%s,%n backImage=%s%n}",
this.providerId,
this.store,
this.label,
this.note,
this.cardId,
this.barcodeType,
this.lastUsed,
this.frontImage,
this.backImage
);
}
}
public static class ZIPData {
public final Map<String, StocardRecord> cards;
public final Map<String, StocardProvider> providers;
ZIPData(final Map<String, StocardRecord> cards, final Map<String, StocardProvider> providers) {
this.cards = cards;
this.providers = providers;
}
}
public static class ImportedData {
public final List<LoyaltyCard> cards;
public final Map<Integer, Map<ImageLocationType, Bitmap>> images;
ImportedData(final List<LoyaltyCard> cards, final Map<Integer, Map<ImageLocationType, Bitmap>> images) {
this.cards = cards;
this.images = images;
}
}
public static final String PROVIDER_PREFIX = "/loyalty-card-providers/";
private static final String TAG = "Catima";
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
ZIPData zipData = new ZIPData(new HashMap<>(), new HashMap<>());
final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.builder().setHeader().build());
try {
for (CSVRecord record : parser) {
StocardProvider provider = new StocardProvider();
provider.name = record.get("name").trim();
provider.barcodeFormat = record.get("barcodeFormat").trim();
zipData.providers.put(record.get("_id").trim(), provider);
}
parser.close();
} catch (IllegalArgumentException | IllegalStateException e) {
throw new FormatException("Issue parsing CSV data", e);
}
ZipFile zipFile = new ZipFile(inputFile, password);
zipData = importZIP(zipFile, zipData);
zipFile.close();
if (zipData.cards.keySet().size() == 0) {
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
}
ImportedData importedData = importLoyaltyCardHashMap(context, zipData);
saveAndDeduplicate(context, database, importedData);
}
public ZIPData importZIP(ZipFile zipFile, final ZIPData zipData) throws IOException, FormatException, JSONException {
Map<String, StocardRecord> cards = zipData.cards;
Map<String, StocardProvider> providers = zipData.providers;
String[] customProvidersBaseName = null;
String[] cardBaseName = null;
String customProviderId = "";
String cardName = "";
for (FileHeader fileHeader : zipFile.getFileHeaders()) {
String fileName = fileHeader.getFileName();
String[] nameParts = fileName.split("/");
if (nameParts.length < 2) {
continue;
}
String userId = nameParts[1];
ZipInputStream zipInputStream = zipFile.getInputStream(fileHeader);
if (customProvidersBaseName == null) {
// FIXME: can we use the points-account/statement/content.json balance info somehow?
/*
Known files:
extracts/<user-UUID>/users/<user-UUID>/
analytics-properties/content.json
devices/<device-UUID>/
analytics-properties/content.json
content.json
ip-location-wifi/content.json
enabled-regions/<UUID>/content.json
loyalty-card-custom-providers/<provider-UUID>/content.json - custom providers
loyalty-cards/<card-UUID>/
card-linked-coupons/accounts/default/
content.json
user-coupons/<UUID>/content.json
content.json - card itself
images/back.png - back image (legacy)
images/back/back.jpg - back image
images/back/content.json
images/front.png - front image (legacy)
images/front/content.json
images/front/front.jpg - front image
notes/default/content.json - note
points-account/
content.json
statement/content.json
usages/<UUID>/content.json - timestamps
usage-statistics/content.json - timestamps
reward-program-balances/<UUID>/content.json
*/
customProvidersBaseName = new String[]{
"extracts",
userId,
"users",
userId,
"loyalty-card-custom-providers"
};
cardBaseName = new String[]{
"extracts",
userId,
"users",
userId,
"loyalty-cards"
};
}
if (startsWith(nameParts, customProvidersBaseName, 1)) {
// Extract providerId
customProviderId = nameParts[customProvidersBaseName.length];
StocardProvider provider = providers.get(customProviderId);
if (provider == null) {
provider = new StocardProvider();
providers.put(customProviderId, provider);
}
// Name file
if (fileName.endsWith(customProviderId + "/content.json")) {
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
provider.name = jsonObject.getString("name");
} else if (fileName.endsWith("logo.png")) {
provider.logo = ZipUtils.readImage(zipInputStream);
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused loyalty-card-custom-providers file " + fileName + ", skipping...");
}
} else if (startsWith(nameParts, cardBaseName, 1)) {
// Extract cardName
cardName = nameParts[cardBaseName.length];
StocardRecord record = cards.get(cardName);
if (record == null) {
record = new StocardRecord();
cards.put(cardName, record);
}
// This is the card itself
if (fileName.endsWith(cardName + "/content.json")) {
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
record.cardId = jsonObject.getString("input_id");
if (jsonObject.has("input_provider_name")) {
record.store = jsonObject.getString("input_provider_name");
}
if (jsonObject.has("label")) {
String label = jsonObject.getString("label");
if (!label.isBlank()) {
record.label = label;
}
}
// Provider ID can be either custom or not, extract whatever version is relevant
String customProviderPrefix = "/users/" + userId + "/loyalty-card-custom-providers/";
String providerId = jsonObject
.getJSONObject("input_provider_reference")
.getString("identifier");
if (providerId.startsWith(customProviderPrefix)) {
providerId = providerId.substring(customProviderPrefix.length());
} else if (providerId.startsWith(PROVIDER_PREFIX)) {
providerId = providerId.substring(PROVIDER_PREFIX.length());
} else {
throw new FormatException("Unsupported provider ID format: " + providerId);
}
record.providerId = providerId;
if (jsonObject.has("input_barcode_format")) {
record.barcodeType = jsonObject.getString("input_barcode_format");
}
} else if (fileName.endsWith("notes/default/content.json")) {
record.note = ZipUtils.readJSON(zipInputStream).getString("content");
} else if (fileName.endsWith("usage-statistics/content.json")) {
JSONArray usages = ZipUtils.readJSON(zipInputStream).getJSONArray("usages");
for (int i = 0; i < usages.length(); i++) {
JSONObject lastUsedObject = usages.getJSONObject(i);
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
if (record.lastUsed == null || timeStamp > record.lastUsed) {
record.lastUsed = timeStamp;
}
}
} else if (fileName.matches(".*/usages/[^/]+/content.json")) {
JSONObject lastUsedObject = ZipUtils.readJSON(zipInputStream);
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
if (record.lastUsed == null || timeStamp > record.lastUsed) {
record.lastUsed = timeStamp;
}
} else if (fileName.endsWith("/images/front.png") || fileName.endsWith("/images/front/front.jpg")) {
record.frontImage = ZipUtils.readImage(zipInputStream);
} else if (fileName.endsWith("/images/back.png") || fileName.endsWith("/images/back/back.jpg")) {
record.backImage = ZipUtils.readImage(zipInputStream);
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused loyalty-cards file " + fileName + ", skipping...");
}
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused file " + fileName + ", skipping...");
}
zipInputStream.close();
}
return new ZIPData(cards, providers);
}
public ImportedData importLoyaltyCardHashMap(Context context, final ZIPData zipData) throws FormatException {
ImportedData importedData = new ImportedData(new ArrayList<>(), new HashMap<>());
int tempID = 0;
List<String> cardKeys = new ArrayList<>(zipData.cards.keySet());
Collections.sort(cardKeys);
for (String key : cardKeys) {
StocardRecord record = zipData.cards.get(key);
if (record.providerId == null) {
Log.d(TAG, "Missing providerId for card " + record + ", ignoring...");
continue;
}
if (record.cardId == null) {
throw new FormatException("No card ID listed, but is required");
}
StocardProvider provider = zipData.providers.get(record.providerId);
// Read store from card, if not available (old export), fall back to providerData
String store = record.store != null ? record.store : provider != null ? provider.name : record.providerId;
String note = record.note != null ? record.note : "";
String barcodeTypeString = record.barcodeType != null ? record.barcodeType : provider != null ? provider.barcodeFormat : null;
if (record.label != null && !record.label.equals(store) && !record.label.equals(note)) {
note = note.isEmpty() ? record.label : note + "\n" + record.label;
}
CatimaBarcode barcodeType = null;
if (barcodeTypeString != null && !barcodeTypeString.isEmpty()) {
if (barcodeTypeString.equals("RSS_DATABAR_EXPANDED")) {
barcodeType = CatimaBarcode.fromBarcode(BarcodeFormat.RSS_EXPANDED);
} else if (barcodeTypeString.equals("GS1_128")) {
barcodeType = CatimaBarcode.fromBarcode(BarcodeFormat.CODE_128);
} else {
barcodeType = CatimaBarcode.fromName(barcodeTypeString);
}
}
int headerColor = Utils.getRandomHeaderColor(context);
if (provider != null && provider.logo != null) {
headerColor = Utils.getHeaderColorFromImage(provider.logo, headerColor);
}
long lastUsed = record.lastUsed != null ? record.lastUsed : Utils.getUnixTime();
LoyaltyCard card = new LoyaltyCard(
tempID,
store,
note,
null,
null,
BigDecimal.valueOf(0),
null,
record.cardId,
null,
barcodeType,
headerColor,
0,
lastUsed,
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
0,
null,
null,
null,
null,
null,
null
);
importedData.cards.add(card);
Map<ImageLocationType, Bitmap> images = new HashMap<>();
if (provider != null && provider.logo != null) {
images.put(ImageLocationType.icon, provider.logo);
}
if (record.frontImage != null) {
images.put(ImageLocationType.front, record.frontImage);
}
if (record.backImage != null) {
images.put(ImageLocationType.back, record.backImage);
}
importedData.images.put(tempID, images);
tempID++;
}
return importedData;
}
public void saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data) throws IOException {
// This format does not have IDs that can cause conflicts
// Proper deduplication for all formats will be implemented later
for (LoyaltyCard card : data.cards) {
// card.id is temporary and only used to index the images Map
long id = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
for (Map.Entry<ImageLocationType, Bitmap> entry : data.images.get(card.id).entrySet()) {
Utils.saveCardImage(context, entry.getValue(), (int) id, entry.getKey());
}
}
}
private boolean startsWith(String[] full, String[] start, int minExtraLength) {
if (full.length - minExtraLength < start.length) {
return false;
}
for (int i = 0; i < start.length; i++) {
if (!start[i].contentEquals(full[i])) {
return false;
}
}
return true;
}
}

View File

@@ -74,6 +74,10 @@ public class Settings {
return getBoolean(R.string.settings_key_display_barcode_max_brightness, true);
}
public String getCardViewOrientation() {
return getString(R.string.settings_key_card_orientation, getResString(R.string.settings_key_follow_system_orientation));
}
public boolean getKeepScreenOn() {
return getBoolean(R.string.settings_key_keep_screen_on, true);
}

View File

@@ -0,0 +1,206 @@
package protect.card_locker.preferences;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.os.LocaleListCompat;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.color.DynamicColors;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
import protect.card_locker.CatimaAppCompatActivity;
import protect.card_locker.MainActivity;
import protect.card_locker.R;
import protect.card_locker.Utils;
import protect.card_locker.databinding.SettingsActivityBinding;
public class SettingsActivity extends CatimaAppCompatActivity {
private SettingsActivityBinding binding;
private final static String RELOAD_MAIN_STATE = "mReloadMain";
private SettingsFragment fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = SettingsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.settings);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
// Display the fragment as the main content.
fragment = new SettingsFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.settings_container, fragment)
.commit();
// restore reload main state
if (savedInstanceState != null) {
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE);
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
finishSettingsActivity();
}
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finishSettingsActivity();
return true;
}
return super.onOptionsItemSelected(item);
}
private void finishSettingsActivity() {
if (fragment.mReloadMain) {
Intent intent = new Intent();
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true);
setResult(Activity.RESULT_OK, intent);
} else {
setResult(Activity.RESULT_OK);
}
finish();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private static final String DIALOG_FRAGMENT_TAG = "SettingsFragment";
public boolean mReloadMain;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
// Show pretty names and summaries
ListPreference themePreference = findPreference(getResources().getString(R.string.settings_key_theme));
assert themePreference != null;
themePreference.setOnPreferenceChangeListener((preference, o) -> {
if (o.toString().equals(getResources().getString(R.string.settings_key_light_theme))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
} else if (o.toString().equals(getResources().getString(R.string.settings_key_dark_theme))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
return true;
});
ListPreference themeColorPreference = findPreference(getResources().getString(R.string.setting_key_theme_color));
assert themeColorPreference != null;
themeColorPreference.setOnPreferenceChangeListener((preference, o) -> {
refreshActivity(true);
return true;
});
if (!DynamicColors.isDynamicColorAvailable()) {
themeColorPreference.setEntryValues(R.array.color_values_no_dynamic);
themeColorPreference.setEntries(R.array.color_value_strings_no_dynamic);
}
Preference oledDarkPreference = findPreference(getResources().getString(R.string.settings_key_oled_dark));
assert oledDarkPreference != null;
oledDarkPreference.setOnPreferenceChangeListener((preference, newValue) -> {
refreshActivity(true);
return true;
});
ListPreference localePreference = findPreference(getResources().getString(R.string.settings_key_locale));
assert localePreference != null;
CharSequence[] entryValues = localePreference.getEntryValues();
List<CharSequence> entries = new ArrayList<>();
for (CharSequence entry : entryValues) {
if (entry.length() == 0) {
entries.add(getResources().getString(R.string.settings_system_locale));
} else {
Locale entryLocale = Utils.stringToLocale(entry.toString());
entries.add(entryLocale.getDisplayName(entryLocale));
}
}
localePreference.setEntries(entries.toArray(new CharSequence[entryValues.length]));
// Make locale picker preference in sync with system settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Locale sysLocale = AppCompatDelegate.getApplicationLocales().get(0);
if (sysLocale == null) {
// Corresponds to "System"
localePreference.setValue("");
} else {
// Need to set preference's value to one of localePreference.getEntryValues() to match the locale.
// Locale.toLanguageTag() theoretically should be one of the values in localePreference.getEntryValues()...
// But it doesn't work for some locales. so trying something more heavyweight.
// Obtain all locales supported by the app.
List<Locale> appLocales = Arrays.stream(localePreference.getEntryValues())
.map(Objects::toString)
.map(Utils::stringToLocale)
.collect(Collectors.toList());
// Get the app locale that best matches the system one
Locale bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale);
// Get its index in supported locales
int index = appLocales.indexOf(bestMatchLocale);
// Set preference value to entry value at that index
localePreference.setValue(localePreference.getEntryValues()[index].toString());
}
}
localePreference.setOnPreferenceChangeListener((preference, newValue) -> {
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
refreshActivity(true);
return true;
}
String newLocale = (String) newValue;
// If newLocale is empty, that means "System" was selected
AppCompatDelegate.setApplicationLocales(newLocale.isEmpty() ? LocaleListCompat.getEmptyLocaleList() : LocaleListCompat.create(Utils.stringToLocale(newLocale)));
return true;
});
// Disable content provider on SDK < 23 since dangerous permissions
// are granted at install-time
Preference contentProviderReadPreference = findPreference(getResources().getString(R.string.settings_key_allow_content_provider_read));
assert contentProviderReadPreference != null;
contentProviderReadPreference.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
}
private void refreshActivity(boolean reloadMain) {
mReloadMain = reloadMain || mReloadMain;
Activity activity = getActivity();
if (activity != null) {
activity.recreate();
}
}
}
}

View File

@@ -1,191 +0,0 @@
package protect.card_locker.preferences
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.color.DynamicColors
import protect.card_locker.BuildConfig
import protect.card_locker.CatimaAppCompatActivity
import protect.card_locker.MainActivity
import protect.card_locker.R
import protect.card_locker.Utils
import protect.card_locker.databinding.SettingsActivityBinding
class SettingsActivity : CatimaAppCompatActivity() {
private lateinit var binding: SettingsActivityBinding
private lateinit var fragment: SettingsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = SettingsActivityBinding.inflate(layoutInflater)
setTitle(R.string.settings)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
val toolbar = binding.toolbar
setSupportActionBar(toolbar)
enableToolbarBackButton()
// Display the fragment as the main content.
fragment = SettingsFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.settings_container, fragment)
.commit()
// restore reload main state
if (savedInstanceState != null) {
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE)
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finishSettingsActivity()
}
})
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
finishSettingsActivity()
return true
}
return super.onOptionsItemSelected(item)
}
private fun finishSettingsActivity() {
if (fragment.mReloadMain) {
val intent = Intent()
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true)
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_OK)
}
finish()
}
class SettingsFragment : PreferenceFragmentCompat() {
var mReloadMain: Boolean = false
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences)
// Show pretty names and summaries
val themePreference = findPreference<ListPreference>(getString(R.string.settings_key_theme))
themePreference!!.setOnPreferenceChangeListener { _, o ->
when (o.toString()) {
getString(R.string.settings_key_light_theme) -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
getString(R.string.settings_key_dark_theme) -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
true
}
val themeColorPreference = findPreference<ListPreference>(getString(R.string.setting_key_theme_color))
themeColorPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity(true)
true
}
if (!DynamicColors.isDynamicColorAvailable()) {
themeColorPreference.setEntryValues(R.array.color_values_no_dynamic)
themeColorPreference.setEntries(R.array.color_value_strings_no_dynamic)
}
val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity(true)
true
}
val localePreference =
findPreference<ListPreference>(getString(R.string.settings_key_locale))!!
localePreference.let {
val entryValues = it.entryValues
val entries = entryValues.map { entry ->
if (entry.isEmpty()) {
getString(R.string.settings_system_locale)
} else {
val entryLocale = Utils.stringToLocale(entry.toString())
entryLocale.getDisplayName(entryLocale)
}
}
it.entries = entries.toTypedArray()
// Make locale picker preference in sync with system settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val sysLocale = AppCompatDelegate.getApplicationLocales()[0]
if (sysLocale == null) {
// Corresponds to "System"
it.value = ""
} else {
// Need to set preference's value to one of localePreference.getEntryValues() to match the locale.
// Locale.toLanguageTag() theoretically should be one of the values in localePreference.getEntryValues()...
// But it doesn't work for some locales. so trying something more heavyweight.
// Obtain all locales supported by the app.
val appLocales = entryValues.map { entry -> Utils.stringToLocale(entry.toString()) }
// Get the app locale that best matches the system one
val bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale)
// Get its index in supported locales
val index = appLocales.indexOf(bestMatchLocale)
// Set preference value to entry value at that index
it.value = entryValues[index].toString()
}
}
}
localePreference.setOnPreferenceChangeListener { _, newValue ->
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
refreshActivity(true)
return@setOnPreferenceChangeListener true
}
val newLocale = newValue as String
// If newLocale is empty, that means "System" was selected
AppCompatDelegate.setApplicationLocales(if (newLocale.isEmpty()) LocaleListCompat.getEmptyLocaleList() else LocaleListCompat.create(Utils.stringToLocale(newLocale)))
true
}
// Disable content provider on SDK < 23 since dangerous permissions
// are granted at install-time
val contentProviderReadPreference = findPreference<Preference>(getString(R.string.settings_key_allow_content_provider_read))
contentProviderReadPreference!!.isVisible =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
// Hide crash reporter settings on builds it's not enabled on
val crashReporterPreference = findPreference<Preference>("acra.enable")
crashReporterPreference!!.isVisible = BuildConfig.useAcraCrashReporter
}
private fun refreshActivity(reloadMain: Boolean) {
mReloadMain = reloadMain || mReloadMain
activity?.recreate()
}
}
companion object {
private const val RELOAD_MAIN_STATE = "mReloadMain"
}
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="#1F4262"/>
</vector>

View File

@@ -3,57 +3,69 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="13.5"
android:translateY="13.5">
<path
android:pathData="M45.5,30.58L68.05,22.37C70.13,21.61 72.42,22.68 73.18,24.76L75.92,32.28L49.6,41.85L45.5,30.58Z"
android:fillColor="#F5A3A3"/>
android:fillColor="#F5A3A3"
android:pathData="M45.5,30.5768L68.0526,22.3683C70.1285,21.6127 72.4239,22.6831 73.1795,24.759L75.9156,32.2765L49.6042,41.8531L45.5,30.5768Z" />
<path
android:pathData="M70.36,25.78C70.17,25.27 69.6,25 69.08,25.19L49.35,32.37L51.4,38.01L72.07,30.48L70.36,25.78ZM75.92,32.28L49.6,41.85L45.5,30.58L68.05,22.37C70.13,21.61 72.42,22.68 73.18,24.76L75.92,32.28Z"
android:fillColor="#CF1717"/>
android:fillColor="#CF1717"
android:pathData="M70.3604,25.785C70.1715,25.2661 69.5977,24.9985 69.0787,25.1874L49.3451,32.3698L51.3973,38.008L72.0705,30.4835L70.3604,25.785ZM75.9156,32.2765L49.6042,41.8531L45.5,30.5768L68.0526,22.3683C70.1285,21.6127 72.4239,22.6831 73.1795,24.759L75.9156,32.2765Z" />
<path
android:pathData="M58.42,30.58L35.86,22.37C33.79,21.61 31.49,22.68 30.74,24.76L28,32.28L54.31,41.85L58.42,30.58Z"
android:fillColor="#F5A3A3"/>
android:fillColor="#F5A3A3"
android:pathData="M58.4155,30.5767L35.8629,22.3682C33.787,21.6126 31.4916,22.683 30.7361,24.7589L27.9999,32.2764L54.3113,41.853L58.4155,30.5767Z" />
<path
android:pathData="M33.56,25.78C33.74,25.27 34.32,25 34.84,25.19L54.57,32.37L52.52,38.01L31.84,30.48L33.56,25.78ZM28,32.28L54.31,41.85L58.42,30.58L35.86,22.37C33.79,21.61 31.49,22.68 30.74,24.76L28,32.28Z"
android:fillColor="#DD1818"/>
android:fillColor="#DD1818"
android:pathData="M33.5551,25.7849C33.744,25.2659 34.3179,24.9984 34.8368,25.1873L54.5704,32.3697L52.5183,38.0078L31.845,30.4834L33.5551,25.7849ZM27.9999,32.2764L54.3113,41.853L58.4155,30.5767L35.8629,22.3682C33.787,21.6126 31.4916,22.683 30.7361,24.7589L27.9999,32.2764Z" />
<path
android:pathData="M28.7,37.6C29.08,35.42 31.15,33.97 33.33,34.35L80.6,42.69C82.78,43.07 84.23,45.15 83.85,47.32L81.82,58.83C82.3,59.1 84.67,60.16 87.42,57.23V57.23C87.92,56.69 88.45,56.23 89.01,55.86C91.76,54.02 94,55.22 94,58.53C94,61.34 92.39,64.78 90.21,66.88C86.63,70.54 80.69,70.56 79.75,70.53L77.59,82.78C77.21,84.95 75.14,86.4 72.96,86.02L25.69,77.69C25.67,77.68 25.66,77.68 25.64,77.68C23.45,77.32 22,76.7 22,76C22,75.72 22.23,75.45 22.66,75.2C22.4,74.54 22.31,73.8 22.44,73.05L28.7,37.6Z"
android:fillColor="#B81414"/>
android:fillColor="#B81414"
android:pathData="M28.6958,37.5992C29.0794,35.4236 31.1543,33.9709 33.3298,34.3545L80.6006,42.6897C82.7761,43.0734 84.2288,45.148 83.8452,47.3235L81.8157,58.8328C82.2998,59.0988 84.6663,60.1572 87.416,57.2288V57.229C87.9156,56.6942 88.4507,56.2283 89.0068,55.8575C91.764,54.0193 93.9993,55.2156 93.9993,58.5293C93.9992,61.343 92.3876,64.7782 90.2134,66.8763C86.626,70.5423 80.6933,70.5552 79.7524,70.5332L77.5938,82.7766C77.2101,84.9522 75.1355,86.4049 72.96,86.0213L25.6892,77.6861C25.6723,77.6831 25.6555,77.6797 25.6387,77.6765C23.4483,77.3198 22.0001,76.7026 22,76.0003C22,75.7175 22.2348,75.4485 22.6582,75.2046C22.3986,74.5429 22.3121,73.8036 22.4446,73.0523L28.6958,37.5992Z" />
<path
android:pathData="M90.67,60.75C90.67,61.85 89.93,63.24 89.01,63.86C88.09,64.47 87.34,64.07 87.34,62.97C87.34,61.86 88.09,60.47 89.01,59.86C89.93,59.24 90.67,59.64 90.67,60.75Z"
android:fillColor="#E82E2E"/>
android:fillColor="#E82E2E"
android:pathData="M90.6707,60.748C90.6707,61.8526 89.9257,63.2447 89.0066,63.8574C88.0876,64.4701 87.3425,64.0714 87.3425,62.9668C87.3425,61.8622 88.0876,60.4701 89.0066,59.8574C89.9257,59.2447 90.6707,59.6434 90.6707,60.748Z" />
<path
android:pathData="M78,30C80.21,30 82,31.79 82,34V70C82,72.21 80.21,74 78,74H30C25.58,74 22,74.9 22,76V32C22,30.9 25.58,30 30,30H78Z"
android:fillColor="#E82E2E"/>
android:fillColor="#E82E2E"
android:pathData="M78,30C80.2091,30 82,31.7909 82,34V70C82,72.2091 80.2091,74 78,74H30C25.5817,74 22,74.8954 22,76V32C22,30.8954 25.5817,30 30,30H78Z" />
<path
android:pathData="M51.2,54.25C51.62,53.53 52.53,53.29 53.25,53.7C53.94,54.1 54.2,54.98 53.84,55.68L53.76,55.82C53.4,56.52 53.65,57.4 54.35,57.8C55.04,58.2 55.93,57.98 56.36,57.32L56.44,57.18C56.87,56.52 57.75,56.3 58.45,56.7C59.16,57.12 59.41,58.03 58.99,58.75C57.75,60.9 55,61.64 52.85,60.4C50.7,59.15 49.96,56.4 51.2,54.25Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M51.2008,54.25C51.615,53.5325 52.5324,53.2867 53.2498,53.7009C53.9449,54.1022 54.1973,54.9757 53.8358,55.6822L53.762,55.8178C53.4005,56.5242 53.6529,57.3977 54.3479,57.799C55.043,58.2003 55.9256,57.9821 56.3567,57.3158L56.4372,57.1841C56.8683,56.5178 57.751,56.2997 58.446,56.7009C59.1634,57.1151 59.4092,58.0325 58.995,58.75C57.7524,60.9023 55.0002,61.6397 52.8479,60.3971C50.6956,59.1544 49.9582,56.4023 51.2008,54.25Z" />
<path
android:pathData="M52.79,54.25C52.38,53.53 51.46,53.29 50.75,53.7C50.05,54.1 49.8,54.98 50.16,55.68L50.23,55.82C50.6,56.52 50.34,57.4 49.65,57.8C48.95,58.2 48.07,57.98 47.64,57.32L47.56,57.18C47.13,56.52 46.24,56.3 45.55,56.7C44.83,57.12 44.59,58.03 45,58.75C46.24,60.9 49,61.64 51.15,60.4C53.3,59.15 54.04,56.4 52.79,54.25Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M52.795,54.25C52.3808,53.5325 51.4634,53.2867 50.746,53.7009C50.051,54.1022 49.7986,54.9757 50.1601,55.6822L50.2339,55.8178C50.5954,56.5242 50.343,57.3977 49.6479,57.799C48.9529,58.2003 48.0702,57.9821 47.6392,57.3158L47.5586,57.1841C47.1276,56.5178 46.2449,56.2997 45.5499,56.7009C44.8324,57.1151 44.5866,58.0325 45.0008,58.75C46.2435,60.9023 48.9956,61.6397 51.1479,60.3971C53.3002,59.1544 54.0377,56.4023 52.795,54.25Z" />
<path
android:pathData="M53.3,56.75C52.72,57.75 51.28,57.75 50.7,56.75L48.1,52.25C47.53,51.25 48.25,50 49.4,50L54.6,50C55.75,50 56.47,51.25 55.9,52.25L53.3,56.75Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M53.2989,56.75C52.7216,57.75 51.2782,57.75 50.7009,56.75L48.1028,52.25C47.5254,51.25 48.2471,50 49.4018,50L54.598,50C55.7527,50 56.4744,51.25 55.897,52.25L53.2989,56.75Z" />
<path
android:pathData="M40.5,40.5C43.73,40.5 46.46,42.62 47.42,45.53C47.68,46.31 47.26,47.16 46.47,47.42C45.69,47.68 44.84,47.26 44.58,46.47C44,44.73 42.38,43.5 40.5,43.5C38.62,43.5 37,44.73 36.42,46.47C36.16,47.26 35.31,47.68 34.53,47.42C33.74,47.16 33.32,46.31 33.58,45.53C34.54,42.62 37.27,40.5 40.5,40.5Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M40.4999,40.5C43.7321,40.5 46.4561,42.6167 47.4233,45.5269C47.6845,46.313 47.2592,47.162 46.4731,47.4233C45.687,47.6846 44.8379,47.2592 44.5766,46.4731C43.9982,44.7328 42.3813,43.5 40.4999,43.5C38.6186,43.5 37.0016,44.7328 36.4233,46.4731C36.162,47.2592 35.3129,47.6846 34.5268,47.4233C33.7407,47.162 33.3153,46.313 33.5766,45.5269C34.5438,42.6167 37.2678,40.5 40.4999,40.5Z" />
<path
android:pathData="M63.5,40.5C66.73,40.5 69.46,42.62 70.42,45.53C70.68,46.31 70.26,47.16 69.47,47.42C68.69,47.68 67.84,47.26 67.58,46.47C67,44.73 65.38,43.5 63.5,43.5C61.62,43.5 60,44.73 59.42,46.47C59.16,47.26 58.31,47.68 57.53,47.42C56.74,47.16 56.32,46.31 56.58,45.53C57.54,42.62 60.27,40.5 63.5,40.5Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M63.4999,40.5C66.7321,40.5 69.4561,42.6167 70.4233,45.5269C70.6845,46.313 70.2592,47.162 69.4731,47.4233C68.687,47.6846 67.8379,47.2592 67.5766,46.4731C66.9982,44.7328 65.3813,43.5 63.4999,43.5C61.6186,43.5 60.0016,44.7328 59.4233,46.4731C59.162,47.2592 58.3129,47.6846 57.5268,47.4233C56.7407,47.162 56.3153,46.313 56.5766,45.5269C57.5438,42.6167 60.2678,40.5 63.4999,40.5Z" />
<path
android:pathData="M26,55C25.45,55 25,54.55 25,54C25,53.45 25.45,53 26,53H42C42.55,53 43,53.45 43,54C43,54.55 42.55,55 42,55H26Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M26,55C25.4477,55 25,54.5523 25,54C25,53.4477 25.4477,53 26,53H42C42.5523,53 43,53.4477 43,54C43,54.5523 42.5523,55 42,55H26Z" />
<path
android:pathData="M26.35,60.94C25.83,61.13 25.26,60.87 25.06,60.35C24.87,59.83 25.13,59.26 25.65,59.06L41.65,53.06C42.17,52.87 42.74,53.13 42.94,53.65C43.13,54.17 42.87,54.74 42.35,54.94L26.35,60.94Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M26.3511,60.9363C25.834,61.1302 25.2576,60.8682 25.0637,60.3511C24.8698,59.834 25.1318,59.2575 25.6489,59.0636L41.6488,53.0637C42.1659,52.8698 42.7423,53.1318 42.9362,53.6489C43.1302,54.166 42.8681,54.7424 42.351,54.9363L26.3511,60.9363Z" />
<path
android:pathData="M61.65,54.94C61.13,54.74 60.87,54.17 61.06,53.65C61.26,53.13 61.83,52.87 62.35,53.06L78.35,59.06C78.87,59.26 79.13,59.83 78.94,60.35C78.74,60.87 78.17,61.13 77.65,60.94L61.65,54.94Z"
android:fillColor="#8A0F0F"/>
android:fillColor="#8A0F0F"
android:pathData="M61.649,54.9364C61.1319,54.7425 60.8699,54.1661 61.0638,53.6489C61.2577,53.1318 61.8341,52.8698 62.3512,53.0637L78.3511,59.0637C78.8682,59.2576 79.1302,59.834 78.9363,60.3511C78.7424,60.8683 78.166,61.1303 77.6489,60.9364L61.649,54.9364Z" />
<path
android:pathData="M78,55C78.55,55 79,54.55 79,54C79,53.45 78.55,53 78,53H62C61.45,53 61,53.45 61,54C61,54.55 61.45,55 62,55H78Z"
android:fillColor="#8A0F0F"/>
</group>
android:fillColor="#8A0F0F"
android:pathData="M78,55C78.5523,55 79,54.5523 79,54C79,53.4477 78.5523,53 78,53H62C61.4477,53 61,53.4477 61,54C61,54.5523 61.4477,55 62,55H78Z" />
</vector>

View File

@@ -3,34 +3,37 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.75"
android:scaleY="0.75"
android:translateX="13.5"
android:translateY="13.5">
<path
android:pathData="M75,31C78.31,31 81,33.69 81,37V44.43C81.83,45.67 82.2,47.22 81.92,48.81L81,54.01V55.87C81.24,55.72 81.5,55.51 81.79,55.22L81.86,55.14C82.38,54.59 82.96,54.08 83.58,53.67L83.72,53.57C85.2,52.64 87.03,52.17 88.68,53.05C90.37,53.96 90.99,55.83 90.99,57.63L90.99,57.77C90.94,60.78 89.29,64.16 87.12,66.26L87.12,66.26C85.22,68.18 82.74,69.08 80.76,69.52C80.64,69.55 80.53,69.57 80.41,69.6C79.85,70.76 78.92,71.72 77.77,72.32L76.71,78.35C76.13,81.61 73.02,83.79 69.76,83.22L30.37,76.27C30.33,76.27 30.29,76.26 30.26,76.25C29.12,76.09 28.06,75.84 27.22,75.49C26.8,75.32 26.33,75.07 25.93,74.72C25.54,74.37 25.01,73.72 25,72.78C25,72.78 25,72.78 25,72.78C25,72.77 25,72.76 25,72.75V34.5C25,33.84 25.32,33.25 25.82,32.89C26.23,32.49 26.7,32.24 27.04,32.09C27.6,31.83 28.27,31.63 28.97,31.48C29.75,31.31 30.64,31.18 31.59,31.1C31.59,31.1 31.58,31.1 31.57,31.1L32.67,28.07L32.73,27.93C33.91,24.91 37.3,23.37 40.36,24.49L52.83,29.03L65.3,24.49C68.42,23.36 71.86,24.96 73,28.07L74.06,31H75ZM34.85,73L70.45,79.28C71.54,79.47 72.58,78.74 72.77,77.66L73.59,73H34.85ZM34,35C32.35,35 30.88,35.15 29.82,35.39C29.48,35.46 29.21,35.54 29,35.61V69.47C30.4,69.17 32.15,69 34,69H75L75.1,69C76.13,68.95 76.95,68.13 77,67.1L77,67V37C77,35.9 76.1,35 75,35H34ZM86.78,56.59C86.68,56.59 86.4,56.61 85.88,56.94L85.8,56.99C85.47,57.21 85.13,57.51 84.78,57.88C84.78,57.88 84.78,57.88 84.78,57.88L84.64,58.03C83.44,59.25 82.18,59.88 81,60.12V65.32C82.2,64.94 83.35,64.36 84.22,63.5L84.34,63.38C85.88,61.89 86.99,59.43 86.99,57.63L86.99,57.53C86.98,56.92 86.84,56.67 86.78,56.59ZM46.56,31L38.99,28.25C37.99,27.88 36.88,28.37 36.47,29.35L36.43,29.44L35.86,31H46.56ZM69.8,31L69.24,29.44C68.86,28.41 67.71,27.87 66.67,28.25L59.11,31H69.8Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
android:fillType="evenOdd"
android:pathData="M75 31.0001C78.3137 31.0001 81 33.6864 81 37.0001V44.4339C81.8278 45.6739 82.1977 47.2224 81.9185 48.8065L81 54.0147V55.8692C81.2385 55.7207 81.5023 55.5135 81.7856 55.2232L81.864 55.1412C82.3815 54.5878 82.9573 54.0824 83.5825 53.6656L83.7239 53.5736C85.1958 52.6378 87.0298 52.1726 88.6753 53.0533C90.3738 53.9624 90.9907 55.8341 90.9907 57.6305L90.9895 57.7735C90.9384 60.7815 89.2885 64.1632 87.1213 66.2555L87.1211 66.2552C85.2162 68.1795 82.7376 69.0841 80.7588 69.5221C80.6417 69.548 80.5255 69.572 80.4106 69.5951C79.8487 70.7645 78.9192 71.7235 77.7715 72.3223L76.709 78.3509C76.1335 81.6141 73.0215 83.7931 69.7583 83.2179L30.3657 76.2718C30.328 76.2652 30.292 76.2576 30.2576 76.2508C29.124 76.0893 28.0601 75.8434 27.2224 75.4942C26.8046 75.32 26.3313 75.075 25.9299 74.7176C25.538 74.3686 25.0098 73.7183 25.0005 72.7752C25.0005 72.7766 25.0007 72.778 25.0007 72.7794C25.0006 72.7696 25 72.7599 25 72.7501V34.5001C25 33.8368 25.3232 33.2494 25.8203 32.8856C26.2266 32.4885 26.6978 32.2406 27.0352 32.0872C27.6048 31.8283 28.2727 31.6301 28.9683 31.4781C29.7466 31.308 30.6353 31.1794 31.5925 31.0987C31.5854 31.0993 31.5784 31.0998 31.5713 31.1004L32.6726 28.0748L32.7275 27.93C33.9136 24.9114 37.2979 23.3732 40.363 24.4888L52.8337 29.0277L65.3047 24.4888C68.4185 23.3555 71.8617 24.9609 72.9951 28.0748L74.0598 31.0001H75ZM34.8474 73.0001L70.4529 79.2784C71.5406 79.4701 72.5777 78.7437 72.7695 77.6561L73.5906 73.0001H34.8474ZM34 35.0001C32.3461 35.0001 30.8829 35.1543 29.8225 35.3861C29.4849 35.4599 29.2118 35.5362 29 35.6082V69.4718C30.4021 69.1686 32.1493 69.0001 34 69.0001H75L75.103 68.9974C76.1256 68.9455 76.9454 68.1256 76.9973 67.1031L77 67.0001V37.0001C77 35.8955 76.1046 35.0001 75 35.0001H34ZM86.782 56.5904C86.6812 56.5851 86.3996 56.6115 85.8818 56.9412L85.8013 56.9937C85.4702 57.2144 85.1253 57.5101 84.7834 57.8761C84.7824 57.8772 84.7811 57.8783 84.78 57.8795L84.6406 58.025C83.4435 59.2483 82.1848 59.8799 81 60.1214V65.3201C82.1999 64.9382 83.3456 64.358 84.219 63.5015L84.343 63.3775C85.8843 61.8895 86.9907 59.4316 86.9907 57.6305L86.9897 57.5343C86.9769 56.9198 86.8422 56.671 86.782 56.5904ZM46.5574 31.0001L38.9949 28.2476C37.9893 27.8817 36.8807 28.3724 36.469 29.347L36.4314 29.4429L35.8645 31.0001H46.5574ZM69.803 31.0001L69.2363 29.4429C68.8586 28.4051 67.7109 27.8698 66.6729 28.2476L59.1104 31.0001H69.803Z" />
<path
android:pathData="M55.17,51C56.32,51 57.04,52.25 56.46,53.25L54.69,56.31C54.6,56.76 54.79,57.24 55.21,57.48C55.73,57.78 56.39,57.6 56.69,57.08C57.11,56.37 58.02,56.12 58.74,56.53C59.46,56.95 59.7,57.87 59.29,58.58C58.16,60.54 55.67,61.21 53.71,60.08C53.45,59.93 53.21,59.75 53,59.55C52.78,59.75 52.54,59.93 52.28,60.08C50.33,61.21 47.83,60.54 46.7,58.58C46.29,57.87 46.53,56.95 47.25,56.53C47.97,56.12 48.88,56.37 49.3,57.08C49.6,57.6 50.26,57.78 50.78,57.48C51.2,57.23 51.4,56.74 51.29,56.29L49.54,53.25C48.96,52.25 49.68,51 50.83,51H55.17Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M55.165 51C56.3197 51.0001 57.0412 52.25 56.4639 53.25L54.6943 56.3145C54.5983 56.7617 54.7947 57.2387 55.2122 57.4797C55.7303 57.7788 56.3927 57.6015 56.6919 57.0835C57.1061 56.3661 58.0235 56.1202 58.741 56.5344C59.4584 56.9487 59.7042 57.8661 59.29 58.5835C58.1625 60.5364 55.6651 61.2054 53.7122 60.0779C53.4495 59.9262 53.2103 59.7495 52.9954 59.553C52.7804 59.7495 52.5414 59.9263 52.2788 60.0779C50.3258 61.2055 47.8283 60.5365 46.7007 58.5835C46.2865 57.8661 46.5324 56.9487 47.2498 56.5344C47.9672 56.1202 48.8846 56.3661 49.2988 57.0835C49.598 57.6016 50.2607 57.7789 50.7788 57.4797C51.2042 57.2341 51.3997 56.7436 51.2905 56.2891L49.5359 53.25C48.9586 52.25 49.6801 51.0001 50.8347 51H55.165Z" />
<path
android:pathData="M43.25,42.5C46,42.5 48.48,44.07 49.4,46.46C49.7,47.24 49.31,48.1 48.54,48.4C47.76,48.7 46.9,48.31 46.6,47.54C46.18,46.44 44.91,45.5 43.25,45.5C41.58,45.5 40.32,46.44 39.9,47.54C39.6,48.31 38.74,48.7 37.96,48.4C37.19,48.1 36.8,47.24 37.1,46.46C38.02,44.07 40.5,42.5 43.25,42.5Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M43.2499 42.5C46.0011 42.5 48.4844 44.0694 49.4008 46.4639C49.6968 47.2376 49.3097 48.1048 48.536 48.4009C47.7623 48.697 46.8951 48.3098 46.599 47.5361C46.1806 46.4427 44.9148 45.5 43.2499 45.5C41.5849 45.5 40.3192 46.4427 39.9008 47.5361C39.6047 48.3098 38.7374 48.697 37.9637 48.4009C37.1901 48.1048 36.8029 47.2376 37.099 46.4639C38.0153 44.0694 40.4987 42.5 43.2499 42.5Z" />
<path
android:pathData="M62.75,42.5C65.5,42.5 67.98,44.07 68.9,46.46C69.2,47.24 68.81,48.1 68.04,48.4C67.26,48.7 66.4,48.31 66.1,47.54C65.68,46.44 64.41,45.5 62.75,45.5C61.08,45.5 59.82,46.44 59.4,47.54C59.1,48.31 58.24,48.7 57.46,48.4C56.69,48.1 56.3,47.24 56.6,46.46C57.52,44.07 60,42.5 62.75,42.5Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M62.7499 42.5C65.5011 42.5 67.9844 44.0694 68.9008 46.4639C69.1968 47.2376 68.8097 48.1048 68.036 48.4009C67.2623 48.697 66.3951 48.3098 66.099 47.5361C65.6806 46.4427 64.4148 45.5 62.7499 45.5C61.0849 45.5 59.8192 46.4427 59.4008 47.5361C59.1047 48.3098 58.2374 48.697 57.4637 48.4009C56.6901 48.1048 56.3029 47.2376 56.599 46.4639C57.5153 44.0694 59.9987 42.5 62.7499 42.5Z" />
<path
android:pathData="M33,55C32.17,55 31.5,54.33 31.5,53.5C31.5,52.67 32.17,52 33,52H44.25C45.08,52 45.75,52.67 45.75,53.5C45.75,54.33 45.08,55 44.25,55H33Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M33 55C32.1716 55 31.5 54.3284 31.5 53.5C31.5 52.6716 32.1716 52 33 52H44.25C45.0784 52 45.75 52.6716 45.75 53.5C45.75 54.3284 45.0784 55 44.25 55H33Z" />
<path
android:pathData="M33.61,59.87C32.85,60.21 31.97,59.87 31.63,59.11C31.29,58.35 31.63,57.47 32.39,57.13L43.61,52.13C44.37,51.79 45.26,52.13 45.59,52.89C45.93,53.65 45.59,54.53 44.83,54.87L33.61,59.87Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M33.6106 59.8701C32.8538 60.2073 31.9671 59.8671 31.6299 59.1104C31.2928 58.3537 31.6329 57.467 32.3896 57.1298L43.6118 52.1298C44.3685 51.7926 45.2553 52.1328 45.5924 52.8895C45.9296 53.6462 45.5894 54.533 44.8327 54.8701L33.6106 59.8701Z" />
<path
android:pathData="M60.85,54.88C60.09,54.55 59.74,53.66 60.07,52.9C60.4,52.14 61.28,51.79 62.04,52.12L73.6,57.12C74.36,57.45 74.71,58.34 74.38,59.1C74.05,59.86 73.16,60.21 72.4,59.88L60.85,54.88Z"
android:fillColor="#000000"/>
android:fillColor="#000000"
android:pathData="M60.8488 54.8768C60.0885 54.5478 59.7389 53.6647 60.0678 52.9044C60.3968 52.1441 61.2798 51.7945 62.0401 52.1234L73.5957 57.1233C74.356 57.4523 74.7056 58.3353 74.3767 59.0956C74.0477 59.8559 73.1647 60.2056 72.4043 59.8766L60.8488 54.8768Z" />
<path
android:pathData="M73,52C73.83,52 74.5,52.67 74.5,53.5C74.5,54.33 73.83,55 73,55H61.5C60.67,55 60,54.33 60,53.5C60,52.67 60.67,52 61.5,52H73Z"
android:fillColor="#000000"/>
</group>
</vector>
android:fillColor="#000000"
android:pathData="M73 52C73.8284 52 74.5 52.6716 74.5 53.5C74.5 54.3284 73.8284 55 73 55H61.5C60.6716 55 60 54.3284 60 53.5C60 52.6716 60.6716 52 61.5 52H73Z" />
</vector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
</adaptive-icon>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>
</adaptive-icon>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -7,92 +7,80 @@ Heimen Stoffels
Oğuz Ersen
FC (Fay) Stegerman
StoyanDimitrov
SlavekB
大王叫我来巡山
Katharine Chui
B o d o
SlavekB
mondstern
IllusiveMan196
大王叫我来巡山
Altonss
Silvério Santos
Edgars Andersons
B o d o
Michael Moroni
Joel A
Eric
Priit Jõerüüt
Joel A
Silvério Santos
Максим Горпиніч
Liner Seven
GM
Petr Novák
Priit Jõerüüt
laralem
GitSpoon
Fjuro
Petr Novák
Edgars Andersons
Taco
nadiafekihahmed
pfaffenrodt
Aayush Gupta
Scrambled777
josé m
ikanakova
Nyatsuki
Giovanni Donisi
Milo Ivir
HudobniVolk
Jiri Grönroos
Vasilis
Warder
Kachelkaiser
Nyatsuki
josé m
Samantaz Fox
Горпиніч Максим Олександрович
Balázs Meskó
Feike Donia
Arno-github
Ankit Tiwari
Milo Ivir
Fjuro
Cliff Heraldo
Sergio Paredes
Ankit Tiwari
Arno-github
Warder
Kachelkaiser
Jose Delvani
109247019824
mdvhimself
Milan Šalka
Robin
தமிழ்நேரம்
huuhaa
GitSpoon
Skrripy
Govindgopalyadav
damjang
Projjal Moitra
Quentin PAGÈS
StellarSand
aradxxx
ngocanhtve
Marnick L'Eau
Vasilis
huuhaa
தமிழ் நேரம்
waffshappen
e-michalak
Marnick L'Eau
ngocanhtve
StellarSand
Quentin PAGÈS
Projjal Moitra
Robin
JungHee Lee
hajertabbane
Denis Shilin
Renko
Ricky Tigg
Robin Liu
Ziad OUALHADJ
delvani
Robin Liu
Ricky Tigg
Renko
Denis Shilin
しいたけ
Alexander Ivanov
Miha Frangež
stavpup
mrestivill
ehrt74
Virginie
Tim Trek
Peter Dave Hello
Aliaksandr Trush
MisterCosta96
arshbeerSingh
Augustin LAVILLE
Freddo espresso
Gideon
n3m0-blip
Kim Seohyun
rudy3
Michael Gangolf
PRATHAMESH BHAGAT
rudy3
Kim Seohyun
Govind S Nair
Freddo espresso
Augustin LAVILLE
arshbeerSingh
MisterCosta96
Aliaksandr Trush

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Catima</string>
<string name="action_search">Soek</string>
<string name="action_add">Voeg by</string>
<string name="save">Stoor</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> geselekteer</item>
<item quantity="other"><xliff:g>%d</xliff:g> geselekteer</item>
</plurals>
</resources>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_search">بحث</string>
<string name="action_add">أضف</string>
<string name="noGiftCards">اضغط على زر الإضافة + لإضافة بطاقة، أو استورد من القائمة خلال ⋮</string>
<string name="noGiftCards">اضغط على زر الإضافة + لإضافة بطاقة، أو استورد من القائمة.</string>
<string name="noMatchingGiftCards">لا نتائج. حاول تغيير كلمات البحث.</string>
<string name="storeName">اسم</string>
<string name="note">مذكرة</string>
@@ -29,7 +29,7 @@
<string name="noCardExistsError">لا يمكن العثور على هذه البطاقة</string>
<string name="failedParsingImportUriError">لا يمكن تحليل الرابط المستورد</string>
<string name="importExport">استيراد/تصدير</string>
<string name="importExportHelp">انشاء نسخة احتياطية من بياناتك يسمح بنقلها إلى جهاز آخر.</string>
<string name="importExportHelp">دعم بياناتك يسمح بنقلها إلى جهاز آخر.</string>
<string name="importFailed">تعذر إجراء الاستيراد</string>
<string name="exportSuccessfulTitle">متصدر</string>
<string name="exportFailedTitle">فشل التصدير</string>
@@ -40,13 +40,16 @@
<string name="app_copyright_old">بناء على Loyalty Card Keychain
\nحقوق النشر © 2016-2020 Branden Archer</string>
<string name="app_license">البرمجيات الحرة متروكة الحقوق, ترخيص +GPLv3</string>
<string name="app_libraries">مكتبات الطرف الثالث : <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">مكتبات الطرف الثالث الحرة: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="selectBarcodeTitle">اختار الباركود</string>
<string name="thumbnailDescription">صورة مصغرة</string>
<string name="starImage">نجم مفضل</string>
<string name="settings">الإعدادات</string>
<string name="settings_light_theme">فاتحة</string>
<string name="settings_dark_theme">داكنة</string>
<string name="settings_card_orientation">اتجاه الشاشة</string>
<string name="settings_portrait_orientation">الوضع الرأسي</string>
<string name="settings_landscape_orientation">الوضع الأفقي</string>
<string name="settings_theme">مظهر</string>
<string name="settings_display_barcode_max_brightness">شاشة ساطعة</string>
<string name="importSuccessful">تم استيراد البيانات</string>
@@ -60,7 +63,7 @@
<string name="group_updated">تم تحديث المجموعة</string>
<string name="all">الكل</string>
<string name="deleteConfirmationGroup">هل تريد حذف المجموعة؟</string>
<string name="failedOpeningFileManager">فشل فتح مدير الملفات</string>
<string name="failedOpeningFileManager">ثبِّت مدير الملفات أولاً.</string>
<string name="moveUp">تحرك لأعلى</string>
<string name="addFromImage">حدد صورة من المعرض</string>
<string name="balance">الرصيد</string>
@@ -74,6 +77,8 @@
<string name="importCatimaMessage">حدّد ملفك <i>catima.zip</i> تصدير من Catima للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير هناك أولاً.</string>
<string name="importFidme">الاستيراد من FidMe</string>
<string name="importFidmeMessage">حدّد ملفك <i>fidme-export-request-xxxxxx.zip</i> تصدير من FidMe للاستيراد، ثم حدد أنواع الباركود يدويًا بعد ذلك. \nإنشئها من ملف تعريف FidMe الخاص بك عن طريق اختيار حماية البيانات ثم الضغط على استخراج بياناتي أولاً.</string>
<string name="importStocardMessage">حدد ملفك <i>***.zip</i> تصدير من Stocard للاستيراد.
\nاحصل عليه عن طريق إرسال بريد إلكتروني إلى support@stocardapp.com لطلب تصدير بياناتك.</string>
<string name="importVoucherVault">الاستيراد من Voucher Vault</string>
<string name="importVoucherVaultMessage">حدّد ملفك <i>vouchervault.json</i> تصدير من Voucher Vault للاستيراد. \nإنشئها بالضغط على تصدير في Voucher Vault أولاً.</string>
<string name="barcodeId">قيمة الباركود</string>
@@ -178,14 +183,16 @@
<string name="about_title_fmt">حول <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">نسخة: <xliff:g id="version">%s</xliff:g></string>
<string name="settings_system_theme">النظام</string>
<string name="settings_lock_on_opening_orientation">قفل على الاتجاه عند فتح البطاقة</string>
<string name="app_resources">موارد الطرف الثالث الحرة: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_system_orientation">نظام المتابعة</string>
<string name="groups">مجموعات</string>
<string name="settings_keep_screen_on">حافظ على الشاشة قيد التشغيل</string>
<string name="intent_import_card_from_url_share_text">اريد مشاركة بطاقة معك</string>
<string name="groupsList">مجموعات: <xliff:g>%s</xliff:g></string>
<string name="settings_disable_lockscreen_while_viewing_card">منع قفل الشاشة</string>
<string name="leaveWithoutSaveTitle">خروج</string>
<string name="editGroup">تعديل المجموعة: <xliff:g>%s</xliff:g></string>
<string name="editGroup">تعديل المجموعه: <xliff:g>%s</xliff:g></string>
<plurals name="groupCardCount">
<item quantity="zero"><xliff:g>%d</xliff:g> بطاقة</item>
<item quantity="one"><xliff:g>%d</xliff:g> بطاقة</item>
@@ -222,7 +229,8 @@
<string name="sort_by_expiry">انقضاء</string>
<string name="importLoyaltyCardKeychain">الاستيراد من Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">حدّد ملفك <i>LoyaltyCardKeychain.csv</i> التصدير من Loyalty Card Keychain للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير في Loyalty Card Keychain بالضغط على تصدير هناك أولاً.</string>
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة</string>
<string name="importStocard">الاستيراد من Stocard</string>
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة. الرجاء الإبلاغ عن هذا.</string>
<string name="help_translate_this_app">ساعد في ترجمة هذا التطبيق</string>
<string name="on_google_play">على Google Play</string>
<string name="settings_theme_color">لون المظهر</string>
@@ -284,6 +292,7 @@
<string name="addWithoutBarcode">إضافة بدون باركود</string>
<string name="field_must_not_be_empty">يجب ألا يكون الحقل فارغا</string>
<string name="app_name">كاتيما</string>
<string name="settings_follow_sensor_orientation">التدوير دائمًا ( تجاهل إعدادات النظام)</string>
<string name="add_manually_warning_title">الفحص موصى به</string>
<string name="continue_">استمر</string>
<string name="spend">انفق</string>
@@ -302,7 +311,7 @@
<string name="useBackImage">استخدم صورة خلفية</string>
<string name="addFromPkpass">اختر ملف الدفتر (.pkpass)</string>
<string name="unsupportedFile">هذا الملف غير مدعوم</string>
<string name="generic_error_please_retry">حدث خطأ ما</string>
<string name="generic_error_please_retry">نعتذر، حدث خطأ ما، حاول مرة أخرى...</string>
<string name="settings_use_volume_keys_navigation">بدّل البطاقات باستخدام أزرار الصوت</string>
<string name="settings_use_volume_keys_navigation_summary">بدّل البطاقات الظاهرة باستخدام أزرار الصوت</string>
<string name="settings_category_title_cards_overview">نظرة عامة على البطاقات</string>
@@ -319,7 +328,4 @@
<string name="sort_by_valid_from">صالح من</string>
<string name="width">العرض</string>
<string name="setBarcodeWidth">تعيين عرض الرمز الشريطي \"باركود\"</string>
<string name="card_list_widget_name">قائمة البطاقات</string>
<string name="cardWithNumber">البطاقة <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">البطاقة <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
</resources>

View File

@@ -84,6 +84,11 @@
<string name="settings_system_theme">Сістэмная</string>
<string name="settings_light_theme">Светлая</string>
<string name="settings_dark_theme">Цёмная</string>
<string name="settings_card_orientation">Арыентацыя экрана</string>
<string name="settings_follow_sensor_orientation">Заўсёды паварочваць (ігнаруе налады сістэмы)</string>
<string name="settings_portrait_orientation">Партрэтная</string>
<string name="settings_landscape_orientation">Альбомная</string>
<string name="settings_lock_on_opening_orientation">Зафіксаваць арыентацыю, якая выкарыстоўваецца пры адкрыцці карты</string>
<string name="settings_keep_screen_on_summary">Адключае тайм-аўт экрана падчас прагляду карты</string>
<string name="settings_disable_lockscreen_while_viewing_card">Прадухіляць блакіроўку экрана</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Адключае блакіроўку экрана падчас прагляду карты</string>
@@ -134,6 +139,7 @@
<string name="importCatima">Імпарт з Catima</string>
<string name="importFidme">Імпарт з FidMe</string>
<string name="importLoyaltyCardKeychain">Імпарт з Loyalty Card Keychain</string>
<string name="importStocard">Імпарт з Stocard</string>
<string name="importVoucherVault">Імпарт з Voucher Vault</string>
<string name="barcodeId">Значэнне штрыхкода</string>
<string name="importVoucherVaultMessage">Каб імпартаваць, выберыце файл <i>vouchervault.json</i> з Voucher Vault. \nСтварыце яго, націснуўшы Экспарт у Voucher Vault .</string>
@@ -262,6 +268,7 @@
<string name="addFromImage">Выбраць малюнак з галерэі</string>
<string name="settings_keep_screen_on">Трымаць экран уключаным</string>
<string name="app_resources">Бясплатныя староннія рэсурсы: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_system_orientation">Як у сістэме</string>
<string name="leaveWithoutSaveTitle">Выйсці</string>
<string name="settings_allow_content_provider_read_title">Дазволіць іншым праграмам доступ да маіх даных</string>
<string name="settings_display_barcode_max_brightness">Павялічваць яркасць экрану</string>
@@ -270,6 +277,7 @@
<string name="editBarcode">Рэдагаваць штрыхкод</string>
<string name="leaveWithoutSaveConfirmation">Выйсці без захавання?</string>
<string name="importLoyaltyCardKeychainMessage">Каб імпартаваць, выберыце файл <i>LoyaltyCardKeychain.csv</i> з Loyalty Card Keychain. \nСтварыце яго з меню «Імпарт/Экспарт» у Loyalty Card Keychain, спачатку націснуўшы там «Экспарт».</string>
<string name="importStocardMessage">Каб імпартаваць, выберыце файл <i>***.zip</i> з Stocard. \nАтрымайце яго па электроннай пошце support@stocardapp.com з запытам на экспарт вашых даных.</string>
<string name="frontImageDescription">Пярэдні відарыс</string>
<string name="groupsList">Групы: <xliff:g>%s</xliff:g></string>
<string name="switchToBackImage">Пераключыцца на задні відарыс</string>

View File

@@ -16,13 +16,13 @@
<string name="note">Бележка</string>
<string name="storeName">Наименование</string>
<string name="noMatchingGiftCards">Няма резултати. Променете критериите за търсене.</string>
<string name="noGiftCards">Докоснете бутона +, за да добавите карта или внесете от менюто ⋮</string>
<string name="noGiftCards">Докоснете бутона +, за да добавите карта или внесете от менюто ⋮.</string>
<string name="all">Всички</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> карта</item>
<item quantity="other"><xliff:g>%d</xliff:g> карти</item>
</plurals>
<string name="failedOpeningFileManager">Грешка при отваряне управление на файлове</string>
<string name="failedOpeningFileManager">Инсталирайте приложение за управление на файлове.</string>
<string name="app_license">Свободен софтуер с авторски права, лицензиран под GPLv3+</string>
<string name="frontImageDescription">Снимка на предната страна</string>
<string name="backImageDescription">Снимка на задната страна</string>
@@ -45,9 +45,10 @@
<string name="sameAsCardId">Като номера</string>
<string name="barcodeId">Стойност на щрихкода</string>
<string name="importLoyaltyCardKeychain">Внасяне от Loyalty Card Keychain</string>
<string name="importFidmeMessage">Изберете предварително изнесен файл от FidMe, който да внесете и ръчно изберете вида на щрихкодовете.\nСъздайте такъв файл от Data Protection в менюто на профила във FidMe и изберете „Extract my data“.</string>
<string name="importFidmeMessage">Изберете файла <i>fidme-export-request-xxxxxx.zip</i>, предварително изнесен от FidMe и ръчно изберете вида на щрихкодовете.
\nСъздайте такъв файл от Data Protection в менюто на профила във FidMe и изберете „Extract my data“.</string>
<string name="importFidme">Внасяне от FidMe</string>
<string name="exportOptionExplanation">Данните ще бъдат запазени на място по ваш избор</string>
<string name="exportOptionExplanation">Данните ще бъдат запазени на място по ваш избор.</string>
<string name="accept">Приемане</string>
<string name="privacy_policy">Политика за личните данни</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
@@ -69,7 +70,7 @@
<string name="expiryStateSentence">Валидност до: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Изтекла: <xliff:g>%s</xliff:g></string>
<string name="balanceSentence">Наличност: <xliff:g>%s</xliff:g></string>
<string name="noGroups">Докоснете бутона +, за да добавите списък</string>
<string name="noGroups">Докоснете бутона +, за да добавите списък.</string>
<string name="groups">Списъци</string>
<string name="enter_group_name">Въведете име на списъка</string>
<string name="intent_import_card_from_url_share_text">Искам да споделя тази карта с вас</string>
@@ -92,21 +93,22 @@
<string name="importFailedTitle">Грешка при внасяне</string>
<string name="exportSuccessfulTitle">Резултат от изнасяне</string>
<string name="importSuccessfulTitle">Резултат от внасяне</string>
<string name="importExportHelp">Резервните копия на данните дават възможност да ги премествате на друго устройство</string>
<string name="importExportHelp">Резервните копия на данните ви дават възможност да ги премествате на друго устройство.</string>
<string name="exportName">Изнасяне</string>
<string name="importExport">Внасяне/изнасяне</string>
<string name="sendLabel">Изпращане…</string>
<string name="scanCardBarcode">Снемане на щрихкод</string>
<string name="editCardTitle">Променяне на карта</string>
<string name="editCardTitle">Редактиране на карта</string>
<string name="share">Споделя</string>
<string name="ok">Добре</string>
<string name="importSuccessful">Данните са внесени</string>
<string name="chooseImportType">Внасяне на данни на</string>
<string name="importCatimaMessage">Изберете предварително изнесен файл от Catima, който да внесете.\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Catima като изберете Изнасяне.</string>
<string name="importCatimaMessage">Изберете файла <i>catima.zip</i>, предварително изнесен от Catima.
\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Catima като изберете Изнасяне.</string>
<string name="importOptionFilesystemButton">Избиране от файлова система</string>
<string name="importOptionFilesystemExplanation">Изберете определен файл от файловата система</string>
<string name="app_resources">Ресурси: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importOptionFilesystemExplanation">Изберете определен файл от файловата система.</string>
<string name="app_resources">Свободни ресурси: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Свободни библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="debug_version_fmt">Издание: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">Относно <xliff:g id="app_name">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Всички права запазени © 2019<xliff:g>%d</xliff:g> Силвия ван Ос и сътрудници</string>
@@ -127,11 +129,16 @@
<string name="addManually">Ръчно въвеждане</string>
<string name="leaveWithoutSaveConfirmation">Оставяте промените незапазени\?</string>
<string name="unsupportedBarcodeType">Щрихкод от този вид не може да бъде показан. Може да бъде поддържан в следващо издание.</string>
<string name="importStocard">Внасяне от Stocard</string>
<string name="importVoucherVault">Внасяне от Voucher Vault</string>
<string name="importVoucherVaultMessage">Изберете предварително изнесен файл от Voucher Vault, който да внесете.\nСъздайте такъв файл от меню „Export“ във Voucher Vault.</string>
<string name="importLoyaltyCardKeychainMessage">Изберете предварително изнесен файл от Loyalty Card Keychain, който да внесете.\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Loyalty Card Keychain като изберете Изнасяне.</string>
<string name="failedParsingImportUriError">Адресът за внасяне не може да бъде анализиран</string>
<string name="failedGeneratingShareURL">Грешка при създаване на адрес, който да споделите</string>
<string name="importVoucherVaultMessage">Изберете файла <i>vouchervault.json</i>, предварително изнесен от Voucher Vault.
\nСъздайте такъв файл от меню „Export“ във Voucher Vault.</string>
<string name="importStocardMessage">Изберете файла <i>***.zip</i>, предварително изнесен от Stocard.
\nПолучете го като изпратите писмо на support@stocardapp.com с искане за изнасяне вашите данни.</string>
<string name="importLoyaltyCardKeychainMessage">Изберете файла <i>LoyaltyCardKeychain.csv</i>, предварително изнесен от Loyalty Card Keychain.
\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Loyalty Card Keychain като изберете Изнасяне.</string>
<string name="failedParsingImportUriError">Препратката не може да бъде анализирана за внасяне</string>
<string name="failedGeneratingShareURL">Не може да бъде генериран адрес за споделяне. Изпратете доклад за дефект.</string>
<string name="deleteTitle">Премахване на карта</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Изтриване на <xliff:g>%d</xliff:g> карта</item>
@@ -181,7 +188,7 @@
<string name="selectColor">Избиране на цвят</string>
<string name="group_name_is_empty">Името на списъка не трябва да е празно</string>
<string name="group_edit">Редактиране на списък</string>
<string name="noGiftCardsGroup">Създайте карти и ги зачислете към списък от тук</string>
<string name="noGiftCardsGroup">Създайте карти и ги зачислите към списък от тук.</string>
<string name="translate_platform">в Weblate</string>
<string name="shortcutSelectCard">Избор на карта</string>
<string name="starred">Със звезда</string>
@@ -193,12 +200,17 @@
</plurals>
<string name="settings_oled_dark">Черен фон за тъмната тема</string>
<string name="include_if_asking_support">Ако искате да потърсите поддръжка, включете следната информация:</string>
<string name="settings_card_orientation">Завъртане на екрана</string>
<string name="settings_follow_system_orientation">Според системата</string>
<string name="settings_portrait_orientation">Портрет</string>
<string name="settings_landscape_orientation">Пейзаж</string>
<string name="settings_lock_on_opening_orientation">Като при отваряне на картата</string>
<string name="duplicateCard">Дублиране</string>
<string name="archive">Архивиране</string>
<string name="unarchive">Изваждане от архива</string>
<string name="archived">Картата е архивирана</string>
<string name="unarchived">Карта е извадена от архива</string>
<string name="failedLaunchingPhotoPicker">Не е намерено поддържано приложение за избор на изображение</string>
<string name="failedLaunchingPhotoPicker">Не е намерено поддържано приложение за галерия</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> карта (<xliff:g id="archivedCount">%2$d</xliff:g> архивирана)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> карти (<xliff:g id="archivedCount">%2$d</xliff:g> архивирани)</item>
@@ -227,8 +239,8 @@
<string name="switchToFrontImage">Показване на предната страна</string>
<string name="switchToBackImage">Показване на задната страна</string>
<string name="switchToBarcode">Показване на щрихкода</string>
<string name="openFrontImageInGalleryApp">Отваряне на изображението на предната страна в приложение за преглед за изображения</string>
<string name="openBackImageInGalleryApp">Отваряне на изображението на задната страна в приложение за преглед за изображения</string>
<string name="openFrontImageInGalleryApp">Отваряне на изображението на предната страна в приложението галерия</string>
<string name="openBackImageInGalleryApp">Отваряне на изображението на задната страна в приложението галерия</string>
<string name="setBarcodeHeight">Задаване на височина на щрихкода</string>
<string name="donate">Даряване</string>
<string name="icon_header_click_text">Задръжте, за да промените миниатюрата</string>
@@ -260,9 +272,10 @@
<string name="addWithoutBarcode">Добавяне на карта без щрихкод</string>
<string name="field_must_not_be_empty">Полето не трябва да е празно</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Винаги да се завърта (пренебрегва системната настройка)</string>
<string name="continue_">Продължаване</string>
<string name="add_manually_warning_title">Препоръчително е да сканирате</string>
<string name="add_manually_warning_message">Стойностите от щрихкода и отбелязаните на картата числа в някои случаи се различават. По тази причина при ръчно въвеждане картата може да не работи. Препоръчително е да сканирате щрихкода с камерата. Желаете ли да продължите въпреки това?</string>
<string name="add_manually_warning_message">Стойностите от щрихкода и отбелязаните на картата числа в някои случаи се различават. По тази причина е при ръчно въвеждане картата може да не работи. Силно препоръчително е да сканирате щрихкода с камерата. Желаете ли да продължите въпреки това?</string>
<string name="amountParsingFailed">Неприемлива сума</string>
<string name="spend">Похарчено</string>
<string name="receive">Получено</string>
@@ -289,20 +302,12 @@
<string name="settings_column_count_landscape">Колони в пейзажен изглед</string>
<string name="settings_column_count_portrait">Колони в портретен изглед</string>
<string name="settings_category_title_cards_overview">Списък с карти</string>
<string name="generic_error_please_retry">Възникна грешка</string>
<string name="addFromPkpass">Изберете файл на Passbook (.pkpass / pkpasses)</string>
<string name="generic_error_please_retry">Съжаляваме, нещо се обърка, опитайте отново…</string>
<string name="addFromPkpass">Изберете файл на Passbook (.pkpass)</string>
<string name="unsupportedFile">Този вид файлове не се поддържат</string>
<string name="sort_by_valid_from">Начало валидност</string>
<string name="width">Ширина</string>
<string name="setBarcodeWidth">Задаване ширина на щрихкода</string>
<string name="setBarcodeWidth">Задаване ширина на щрих кода</string>
<string name="card_list_widget_name">Списък с карти</string>
<string name="card_list_widget_empty">Когато добавите карти в Catima те ще се покажат тук. Ако имате карти уверете се, че са извън архива.</string>
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Не завъртайте устройството, защото това ще прекъсне действието</string>
<string name="acra_catima_has_crashed">За съжаление <xliff:g id="app_name">%s</xliff:g> се срина. Помогнете ни да оправим проблема като ни изпратите доклад за грешката.</string>
<string name="acra_crash_email_subject">Доклад за срив на <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Питане преди изпращане на доклад за срив</string>
<string name="pref_enable_acra_summary">Когато е отметнато, при срив ще ви бъде предложено да докладвате за него. Докладите никога не се изпращат автоматично.</string>
<string name="acra_explain_crash">Ако е възможно добавете подробности за вашите действия:</string>
</resources>

View File

@@ -26,6 +26,7 @@
<string name="starImage">তারা ছবি</string>
<string name="importCatima">ক্যাতিনা আগম</string>
<string name="importLoyaltyCardKeychain">আমদানি লয়্যালটি কার্ড কীচেন</string>
<string name="importStocard">স্টো কার্ড আমদানি করুন</string>
<string name="importVoucherVault">আমদানি ভাউচার ভল্ট</string>
<string name="barcodeId">বারকোড আইডি</string>
<string name="sameAsCardId">আইডি আর এটা এক</string>
@@ -110,6 +111,9 @@
<string name="about_title_fmt"><xliff:g id="app_name">%s</xliff:g>টির সম্পর্কে</string>
<string name="app_resources">মুক্ত সম্পদ যেগুলি আমার নয়: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="thumbnailDescription">থাম্বনেইল</string>
<string name="settings_card_orientation">বারকোড অভিমুখ</string>
<string name="settings_follow_system_orientation">সিস্টেমের অনুসারে</string>
<string name="settings_portrait_orientation">প্রতিকৃতি</string>
<string name="barcodeImageDescriptionWithType">ছবি <xliff:g>%s</xliff:g> বারকোড</string>
<string name="exportName">রপ্তানি</string>
<string name="failedParsingImportUriError">আমদানির URI-টি বোঝা যাচ্ছে না</string>
@@ -135,6 +139,8 @@
<string name="selectBarcodeTitle">বারকোড নির্বাচন করুন</string>
<string name="settings">সেটিংস</string>
<string name="settings_dark_theme">অন্ধকার</string>
<string name="settings_landscape_orientation">অনুভূমিক</string>
<string name="settings_lock_on_opening_orientation">কার্ড খোলার সময় যে অভিমুখ থাকে সেটিতে লক করে দেবেন</string>
<string name="group_name_already_in_use">গ্রুপটির নাম আগে একবার ব্যবহার করে ফেলেছেন</string>
<string name="group_edit">গ্রুপ সম্পাদনা করুন</string>
<string name="group_updated">গ্রুপটি আপডেট করা হল</string>
@@ -197,6 +203,8 @@
\nআপনার FidMe প্রোফাইল থেকে ডেটা সুরক্ষা নির্বাচন করে এবং তারপর প্রথমে আমার ডেটা বের করুন টিপে এটি তৈরি করুন।</string>
<string name="importCatimaMessage">ক্যাটিমা থেকে আমদানি করতে আপনার <i>catima.zip</i> রপ্তানি নির্বাচন করুন।
\nঅন্য Catima অ্যাপের আমদানি/রপ্তানি মেনু থেকে প্রথমে সেখানে রপ্তানি টিপে এটি তৈরি করুন।</string>
<string name="importStocardMessage">আমদানি করতে Stocard থেকে আপনার <i>***.zip</i> এক্সপোর্ট নির্বাচন করুন।
\nআপনার ডেটা রপ্তানির জন্য জিজ্ঞাসা করে support@stocardapp.com ই-মেইল করে এটি পান।</string>
<string name="importVoucherVaultMessage">আমদানি করতে ভাউচার ভল্ট থেকে আপনার <i>vouchervault.json</i> এক্সপোর্ট নির্বাচন করুন।
\nপ্রথমে ভাউচার ভল্টে এক্সপোর্ট টিপে এটি তৈরি করুন।</string>
<string name="settings_oled_dark">অন্ধকার থিমের জন্য খাঁটি কালো পটভূমি</string>

View File

@@ -73,5 +73,6 @@
<string name="permissionReadCardsLabel">কাটিমা কার্ডস পড়ুন</string>
<string name="storageReadPermissionRequired">এই কাজটির জন্য ফোনের স্টোরেজ দেখার অনুমতি লাগবে…</string>
<string name="exportFailedTitle">রপ্তানি ব্যর্থ</string>
<string name="settings_card_orientation">বারকোড অভিমুখ (ওরিয়েন্টেশন)</string>
<string name="app_name">ক্যাটিমা</string>
</resources>

View File

@@ -26,6 +26,7 @@
<string name="starImage">Omiljena zvijezda</string>
<string name="importCatima">Uvezi iz Catima</string>
<string name="importLoyaltyCardKeychain">Uvezi iz Loyalty Card Keychain</string>
<string name="importStocard">Uvezi iz Stokarda</string>
<string name="importVoucherVault">Uvezi iz trezora vaučer</string>
<string name="barcodeId">Barcode vrijednost</string>
<string name="sameAsCardId">Isto kao i kartica</string>

View File

@@ -7,12 +7,12 @@
<string name="delete">Elimina</string>
<string name="confirm">Confirma</string>
<string name="ok">D\'acord</string>
<string name="importExport">Importa/exporta</string>
<string name="importExport">Importa/Exporta</string>
<string name="exportName">Exporta</string>
<string name="action_search">Cerca</string>
<string name="deleteTitle">Elimina la targeta</string>
<string name="welcome">Benvingut a Catima</string>
<string name="noGiftCards">Fes clic al botó + per afegir una targeta, o importa des del menú</string>
<string name="noGiftCards">Cliqueu el botó + més per afegir una targeta, o importeu-ne des del menú.</string>
<string name="photos">Fotos</string>
<string name="app_name">Catima</string>
<string name="moveDown">Baixar abaix</string>
@@ -24,10 +24,10 @@
<string name="on_google_play">al Google Play</string>
<string name="settings_locale">Idioma</string>
<string name="field_must_not_be_empty">El camp no pot estar buit</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os i col·laboradors</string>
<string name="app_copyright_short">Copyright © Sylvia van Os i col·laboradors</string>
<string name="app_license">Programari lliure Copyleft, licència GPLv3+</string>
<string name="app_resources">Recursos de tercers: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os i contribuïdors</string>
<string name="app_copyright_short">Copyright © Sylvia van Os i contribuïdors</string>
<string name="app_license">Software lliure Copyleft, licència GPLv3+</string>
<string name="app_resources">Recursos lliures de tercers: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="thumbnailDescription">Miniatura</string>
<string name="starImage">Estrella de preferides</string>
<string name="settings">Configuració</string>
@@ -35,6 +35,7 @@
<string name="settings_light_theme">Tema clar</string>
<string name="settings_system_theme">Tema de sistema</string>
<string name="settings_dark_theme">Tema Fosc</string>
<string name="settings_card_orientation">Orientació de la pantalla</string>
<string name="settings_allow_content_provider_read_title">Permet altres apps a accedir a les meves dades</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Desactiva el bloqueix la pantalla mentre es visualitza la targeta</string>
<string name="settings_allow_content_provider_read_summary">Les aplicacions han de seguir demanant permís per tenir-hi accés</string>
@@ -55,13 +56,13 @@
<string name="add_manually_warning_title">Recomenem escanejar</string>
<string name="add_manually_warning_message">En algunes targetes el valor imprès en la targeta no correspon amb el codi registrat en el codi de barres. Per això, introduint manualment el codi pot no funcionar en alguns casos. Recomanem sempre que sigui possible escanejar la targeta amb la càmera. Vol igualment continuar la edició manual?</string>
<string name="continue_">Continuar</string>
<string name="exportOptionExplanation">La informació serà escrita al lloc de la seva elecció</string>
<string name="exportOptionExplanation">La informació serà escrita al lloc de la seva elecció.</string>
<string name="importOptionFilesystemTitle">Importar desde el sistema de fitxers</string>
<string name="importOptionFilesystemButton">Desde el sistema de fitxers</string>
<string name="selectBarcodeTitle">Selecciona el codi de barres</string>
<string name="selectBarcodeTitle">Sel•lecciona el Codi de Barres</string>
<string name="importSuccessful">Dades importades correctament</string>
<string name="exportSuccessful">Dades exportades correctament</string>
<string name="failedOpeningFileManager">No s\'ha pogut obrir el gestor de fitxers</string>
<string name="failedOpeningFileManager">Instala un gestor de fitxers.</string>
<string name="showMoreInfo">Mostrar informació</string>
<string name="version_history">Històric de versions</string>
<string name="sort_by">Ordenar per</string>
@@ -72,7 +73,7 @@
<item quantity="many"><xliff:g>%d</xliff:g> seleccionats</item>
<item quantity="other"><xliff:g>%d</xliff:g> seleccionats</item>
</plurals>
<string name="importOptionFilesystemExplanation">Escull un fitxer especific del sistema de fitxers</string>
<string name="importOptionFilesystemExplanation">Escull un fitxer especific del sistema de fitxers.</string>
<string name="no">No</string>
<string name="settings_pink_theme">Rosa</string>
<string name="sort">Ordenar</string>
@@ -96,19 +97,22 @@
</plurals>
<string name="importCancelled">Importació anulada</string>
<string name="exportCancelled">Exportació cancelada</string>
<string name="noGiftCardsGroup">Crea algunes targetes i després asigna-les en al grup aquí</string>
<string name="noMatchingGiftCards">No hi ha resultats; prova de modificar la cerca.</string>
<string name="noGiftCardsGroup">Crea algunes targetes, asigna-les en un grup aquí.</string>
<string name="noMatchingGiftCards">Sense resultats. Prova a canviar la teva cerca.</string>
<string name="storeName">Nom</string>
<string name="note">Nota</string>
<string name="cardId">Id. de la Targeta</string>
<string name="barcodeType">Tipus de codi de barres</string>
<string name="noBarcode">Sense codi de barres</string>
<string name="settings_portrait_orientation">Vertical</string>
<string name="yes">Si</string>
<string name="addFromPdfFile">Seleccioni un PDF</string>
<string name="errorReadingFile">No s\'ha pogut llegir el fitxer</string>
<string name="failedLaunchingFileManager">No s\'ha pogut trobar un gestor de fitxers compatible</string>
<string name="multipleBarcodesFoundPleaseChooseOne">Quin dels següents codis de barres prefereix utilitzar?</string>
<string name="pageWithNumber">Pàgina <xliff:g>%d</xliff:g></string>
<string name="settings_follow_system_orientation">Seguir el sistema</string>
<string name="settings_landscape_orientation">Horitzontal</string>
<string name="intent_import_card_from_url_share_text">Vull compartir una targeta amb tu</string>
<string name="takePhoto">Fer una foto</string>
<string name="help_translate_this_app">Ajuda a traduïr aquesta app</string>
@@ -130,6 +134,7 @@
<string name="barcodeImageDescriptionWithType">Codi de barres <xliff:g>%s</xliff:g></string>
<string name="about_title_fmt">Sobre <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Versió: <xliff:g id="version">%s</xliff:g></string>
<string name="settings_follow_sensor_orientation">Sempre rota (ignora la configuració de sistema)</string>
<string name="settings_display_barcode_max_brightness_summary">Alguns escàners ho necesiten</string>
<string name="settings_keep_screen_on">Mantenir la pantalla encesa</string>
<string name="settings_keep_screen_on_summary">Desactiva el bloqueix de la pantalla mentre mostra una targeta</string>
@@ -166,21 +171,22 @@
<string name="deleteConfirmation">Vols eliminar de forma permanent aquesta targeta?</string>
<string name="share">Compartir</string>
<string name="sendLabel">Enviar…</string>
<string name="editCardTitle">Editar targeta</string>
<string name="addCardTitle">Afegir targeta</string>
<string name="scanCardBarcode">Escanejar codi de barres</string>
<string name="cardShortcut">Drecera a la targeta</string>
<string name="editCardTitle">Editar Targeta</string>
<string name="addCardTitle">Afegir Targeta</string>
<string name="scanCardBarcode">Escanejar Codi de Barres</string>
<string name="cardShortcut">Drecera a la Targeta</string>
<string name="noCardsMessage">Afegeix primer una targeta</string>
<string name="noCardExistsError">No s\'ha pogut trobar aquesta targeta</string>
<string name="failedParsingImportUriError">No s\'ha pogut analitzar l\'URI d\'importació</string>
<string name="failedParsingImportUriError">No s\'ha pogut analitzar la URI d\'importació</string>
<string name="openFrontImageInGalleryApp">Obrir la imatge frontal a l\'app de galeria</string>
<string name="settings_lock_on_opening_orientation">En obrir la targeta, bloquejar la orientació de la pantalla</string>
<string name="settings_use_volume_keys_navigation_summary">Utilitza els botons de volum per canviar la targeta que es mostra</string>
<string name="updateBarcodeQuestionText">Ha canviat el valor ID. Vol actualitzar també el codi de barres per uter utilitzar el mateix valor?</string>
<string name="settings_sky_blue_theme">Blau fluix</string>
<string name="starred">Preferides</string>
<string name="deleteConfirmationGroup">Vols eliminar aquest grup?</string>
<string name="removeImage">Eliminar imatge</string>
<string name="app_libraries">Llibreries de tercers: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Llibreries lliures de tercers: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="settings_display_barcode_max_brightness">Màxima iluminació</string>
<string name="settings_brown_theme">Marró</string>
<string name="manually_enter_barcode_instructions">Introdueixi el ID de la targeta manualment i trii un codi de barres que s\'assembli al de la seva targeta.</string>
@@ -227,7 +233,7 @@
<string name="addFromPkpass">Seleccioni el fitxer Passbook (.pkpass)</string>
<string name="unsupportedFile">Aquest fitxer no està soportat</string>
<string name="settings_use_volume_keys_navigation">Canviar les targetes al prèmer els botons de volum</string>
<string name="noGroups">Feu clic al botó + més per aferir grups pre categoritzar</string>
<string name="noGroups">Clica el botó + per afegir grups per categoritzar.</string>
<string name="noGroupCards">Aquest grup està buit</string>
<string name="group_name_already_in_use">Ja existeix un grup amb aquest nom</string>
<string name="group_updated">Grup actualitzat</string>
@@ -238,43 +244,4 @@
<string name="settings_system_locale">Idioma del sistema</string>
<string name="settings_catima_theme">Catima</string>
<string name="spend">Gastar</string>
<string name="importExportHelp">Fer una còpia de seguretat de les dades permet moure-les a un altre dispositiu</string>
<string name="importSuccessfulTitle">Importat</string>
<string name="importFailedTitle">La importació ha fallat</string>
<string name="importFailed">No s\'ha pogut realitzar la importació</string>
<string name="exportSuccessfulTitle">Exportat</string>
<string name="exportFailedTitle">L\'exportació ha fallat</string>
<string name="exportFailed">No s\'ha pogut realitzar l\'exportació</string>
<string name="importing">Important…</string>
<string name="exporting">Exportant…</string>
<string name="storageReadPermissionRequired">Cal permís per llegir l\'emmagatzematge per a aquesta acció…</string>
<string name="cameraPermissionRequired">Cal permís per accedir a la càmera per a aquesta acció…</string>
<string name="permissionReadCardsLabel">Legeix targetes Catima</string>
<string name="permissionReadCardsDescription">llegeix les teves targetes Catima i tots els seus detalls, incloses notes i imatges</string>
<string name="cameraPermissionDeniedTitle">No s\'ha pogut accedir a la càmera</string>
<string name="noCameraPermissionDirectToSystemSetting">Per escanejar codis de barres, Catima necessitarà accés a la teva càmera. Toca aquí per canviar la configuració dels permisos.</string>
<string name="about">Sobre</string>
<string name="app_copyright_old">Clauer basat en na Loyalty Card Keychain\ncopyright © 20162020 Branden Archer</string>
<string name="addManually">Introduïu el codi de barres manualment</string>
<string name="addFromImage">Seleccioneu una imatge de la galeria</string>
<string name="groupsList">Grups: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Editeu el grup: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Caduca el: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Caducat el: <xliff:g>%s</xliff:g></string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punt</item>
<item quantity="many"><xliff:g>%s</xliff:g> punts</item>
<item quantity="other"/>
</plurals>
<string name="balanceSentence">Saldo: <xliff:g>%s</xliff:g></string>
<string name="card">Targeta</string>
<string name="editBarcode">Editeu el codi de barres</string>
<string name="expiryDate">Data de caducitat</string>
<string name="never">Mai</string>
<string name="chooseExpiryDate">Trieu la data de caducitat</string>
<string name="moveBarcodeToTopOfScreen">Moveu el codi de barres a la part superior de la pantalla</string>
<string name="noBarcodeFound">No s\'ha trobat cap codi de barres</string>
<string name="errorReadingImage">No s\'ha pogut llegir la imatge</string>
<string name="balance">Saldo</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Přidat</string>
<string name="noGiftCards">Klepněte na tlačítko plus (+) pro přidání karty nebo naimportujete karty z nabídky (⋮)</string>
<string name="noGiftCards">Klepněte na tlačítko plus (+) pro přidání karty nebo naimportujete karty z nabídky (⋮).</string>
<string name="storeName">Název</string>
<string name="note">Poznámka</string>
<string name="cardId">ID karty</string>
@@ -12,12 +12,12 @@
<string name="confirm">Potvrdit</string>
<string name="ok">OK</string>
<string name="sendLabel">Odeslat…</string>
<string name="editCardTitle">Upravit kartu</string>
<string name="editCardTitle">Editovat kartu</string>
<string name="addCardTitle">Přidat kartu</string>
<string name="scanCardBarcode">Naskenovat čárový kód</string>
<string name="importExport">Import/export</string>
<string name="importExport">Import/Export</string>
<string name="exportName">Export</string>
<string name="importExportHelp">Zálohování dat vám umožní přesunout je do jiného zařízení</string>
<string name="importExportHelp">Zálohování dat vám umožní přesunout je do jiného zařízení.</string>
<string name="importSuccessfulTitle">Importováno</string>
<string name="importFailedTitle">Import selhal</string>
<string name="importFailed">Import nelze provést</string>
@@ -27,7 +27,7 @@
<string name="importing">Importuji…</string>
<string name="exporting">Exportuji…</string>
<string name="importOptionFilesystemTitle">Import z úložiště</string>
<string name="importOptionFilesystemExplanation">Vyberte konkrétní soubor v úložišti</string>
<string name="importOptionFilesystemExplanation">Vyberte konkrétní soubor v úložišti.</string>
<string name="importOptionFilesystemButton">Z úložiště</string>
<string name="about">O aplikaci</string>
<string name="app_license">Copyleftovaný svobodný software s licencí GPLv3+</string>
@@ -41,13 +41,13 @@
<string name="never">Nikdy</string>
<string name="expiryDate">Vypršení platnosti</string>
<string name="editBarcode">Upravit čárový kód</string>
<string name="app_resources">Zdroje třetích stran: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Knihovny třetích stran: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Svobodné zdroje třetích stran: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Svobodné knihovny třetích stran: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_old">Založeno na Loyalty Card Keychain
\ncopyright © 20162020 Branden Archer</string>
<string name="exportOptionExplanation">Data budou zapsána na místo podle vašeho výběru</string>
<string name="failedParsingImportUriError">Nepodařilo se zpracovat URI importu</string>
<string name="noCardExistsError">Kartu se nepodařilo najít</string>
<string name="exportOptionExplanation">Data budou zapsána na místo podle vašeho výběru.</string>
<string name="failedParsingImportUriError">Nelze zpracovat URI importu</string>
<string name="noCardExistsError">Tuto kartu nelze najít</string>
<string name="noCardsMessage">Nejprve přidejte kartu</string>
<string name="cardShortcut">Zástupce karty</string>
<string name="share">Sdílet</string>
@@ -96,8 +96,8 @@
<string name="settings_locale">Jazyk</string>
<string name="turn_flashlight_off">Vypnout světlo</string>
<string name="turn_flashlight_on">Zapnout světlo</string>
<string name="failedGeneratingShareURL">Nepodařilo se vygenerovat adresu URL pro sdílení</string>
<string name="passwordRequired">Zadejte heslo</string>
<string name="failedGeneratingShareURL">Nepodařilo se vygenerovat adresu URL pro sdílení. Nahlaste to prosím.</string>
<string name="passwordRequired">Zadejte prosím heslo</string>
<string name="no">Ne</string>
<string name="yes">Ano</string>
<string name="updateBarcodeQuestionText">Změnili jste ID. Chcete také aktualizovat čárový kód, aby používal stejnou hodnotu\?</string>
@@ -115,29 +115,36 @@
<string name="barcodeId">Hodnota čárového kódu</string>
<string name="setBarcodeId">Nastavení hodnoty čárového kódu</string>
<string name="sameAsCardId">Stejné jako ID</string>
<string name="importVoucherVaultMessage">Vyberte soubor exportu z aplikace Voucher Vault, který chcete importovat.\nVytvořte jej z nabídky stisknutím tlačítka Export v aplikaci Voucher Vault.</string>
<string name="importVoucherVaultMessage">Vyberte k importu svůj <i>vouchervault.json</i> exportovaný z Voucher Vault.
\nVytvoříte jej tak, že nejprve stisknete tlačítko Exportovat v aplikaci Voucher Vault.</string>
<string name="importVoucherVault">Import z Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte soubor exportu z aplikace Loyalty Card Keychain, který chcete importovat.\nVytvořte jej z nabídky Import/export jiné aplikace Loyalty Card Keychain klepnutím na Export.</string>
<string name="importStocardMessage">Vyberte k importu svůj <i>***.zip</i> exportovaný z aplikace Stocard.
\nZískejte ji zasláním e-mailu na adresu support@stocardapp.com s žádostí o export vašich dat.</string>
<string name="importStocard">Import ze Stocard</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte k importu <i>LoyaltyCardKeychain.csv</i> exportovaný z Loyalty Card Keychain.
\nVytvoříte jej z nabídky Import/Export v Loyalty Card Keychain tak, že tam nejprve stisknete tlačítko Exportovat.</string>
<string name="importLoyaltyCardKeychain">Import z Loyalty Card Keychain</string>
<string name="importFidmeMessage">Vyberte soubor exportu z aplikace FidMe, který chcete importovat, a poté ručně vyberte typy čárových kódů.\nVytvořte jej z aplikace FidMe vybráním položky Data Protection a stisknutím tlačítka Extract my data.</string>
<string name="importFidmeMessage">Vyberte k importu svůj <i>fidme-export-request-xxxxxx.zip</i> exportovaný z FidMe a poté vyberte typy čárových kódů ručně.
\nVytvoříte jej ze svého profilu FidMe tak, že nejprve zvolíte možnost Ochrana dat a poté stisknete tlačítko Extrahovat moje data.</string>
<string name="importFidme">Import z FidMe</string>
<string name="importCatimaMessage">Vyberte soubor exportu z aplikace Catima, který chcete importovat.\nVytvořte jej z nabídky Import/export jiné aplikace Catima klepnutím na Export.</string>
<string name="importCatimaMessage">Vyberte <i>catima.zip</i> exportovaný z aplikace Catima, který chcete importovat.
\nVytvoříte jej z nabídky Import/Export jiné aplikace Catima tak, že v ní nejprve stisknete tlačítko Exportovat.</string>
<string name="importCatima">Import z Catima</string>
<string name="accept">Přijmout</string>
<string name="privacy_policy">Ochrana soukromí</string>
<string name="privacy_policy">Zásady soukromí</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">Importovat data z</string>
<string name="points">Body</string>
<string name="currency">Měna</string>
<string name="balance">Zůstatek</string>
<string name="errorReadingImage">Nepodařilo se přečíst obrázek</string>
<string name="errorReadingImage">Obrázek se nepodařilo přečíst</string>
<string name="noBarcodeFound">Čárový kód nenalezen</string>
<string name="groupsList">Skupiny: <xliff:g>%s</xliff:g></string>
<string name="addFromImage">Vybrat obrázek z galerie</string>
<string name="addManually">Zadat čárový kód ručně</string>
<string name="leaveWithoutSaveConfirmation">Ukončit bez uložení\?</string>
<string name="leaveWithoutSaveTitle">Ukončit</string>
<string name="failedOpeningFileManager">Nepodařilo se otevřít správce souborů</string>
<string name="failedOpeningFileManager">Nejprve si nainstalujte správce souborů.</string>
<string name="deleteConfirmationGroup">Smazat skupinu\?</string>
<string name="all">Všechny</string>
<plurals name="groupCardCount">
@@ -145,7 +152,7 @@
<item quantity="few"><xliff:g>%d</xliff:g> karty</item>
<item quantity="other"><xliff:g>%d</xliff:g> karet</item>
</plurals>
<string name="noGroups">Klepnutím na tlačítko plus (+) přidejte skupiny pro kategorizaci</string>
<string name="noGroups">Kliknutím na tlačítko + plus přidejte skupiny pro kategorizaci.</string>
<string name="groups">Skupiny</string>
<string name="enter_group_name">Zadejte název skupiny</string>
<string name="exportSuccessful">Data exportována</string>
@@ -170,7 +177,7 @@
<string name="and_data_usage">a využití dat</string>
<string name="credits">Zásluhy</string>
<string name="on_github">na GitHubu</string>
<string name="source_repository">Zdrojový repozitář</string>
<string name="source_repository">Úložiště zdrojů</string>
<string name="license">Licence</string>
<string name="help_translate_this_app">Pomozte s překladem této aplikace</string>
<string name="report_error">Nahlásit chybu</string>
@@ -184,7 +191,7 @@
<string name="group_name_is_empty">Název skupiny nesmí být prázdný</string>
<string name="group_updated">Skupina aktualizována</string>
<string name="editGroup">Úprava skupiny: <xliff:g>%s</xliff:g></string>
<string name="noGiftCardsGroup">Vytvořte si karty a poté je zde přiřaďte do skupiny</string>
<string name="noGiftCardsGroup">Zatím nemáte žádné věrnostní karty. Jakmile nějaké přidáte, můžete je zde přiřadit do skupiny.</string>
<string name="shortcutSelectCard">Vybrat kartu</string>
<string name="translate_platform">na Weblate</string>
<string name="showMoreInfo">Zobrazit podrobnosti</string>
@@ -197,12 +204,17 @@
</plurals>
<string name="settings_oled_dark">Čistě černé pozadí pro tmavý motiv</string>
<string name="include_if_asking_support">Pokud chcete požádat o podporu, uveďte následující informace:</string>
<string name="settings_follow_system_orientation">Podle orientace systému</string>
<string name="settings_portrait_orientation">Na výšku</string>
<string name="settings_lock_on_opening_orientation">Ponechat orientaci jako při otevření karty</string>
<string name="archive">Archivovat</string>
<string name="unarchive">Vrátit z archivu</string>
<string name="unarchived">Karta vrácena z archivu</string>
<string name="settings_card_orientation">Orientace obrazovky</string>
<string name="settings_landscape_orientation">Na šířku</string>
<string name="duplicateCard">Duplikovat</string>
<string name="archived">Karta archivována</string>
<string name="failedLaunchingPhotoPicker">Nepodařilo se najít podporovaný nástroj pro výběr obrázků</string>
<string name="failedLaunchingPhotoPicker">Nepodařilo se najít podporovanou aplikaci galerie</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> karta (<xliff:g id="archivedCount">%2$d</xliff:g> archivovaná)</item>
<item quantity="few"><xliff:g>%1$d</xliff:g> karty (<xliff:g id="archivedCount">%2$d</xliff:g> archivované)</item>
@@ -214,7 +226,7 @@
<string name="welcome">Vítejte v Catima</string>
<string name="barcodeLongPressMessage">V aplikaci pro galerii mohou být otevírány pouze obrázky</string>
<string name="failedToRetrieveImageFile">Nepodařilo se získat soubor obrázku</string>
<string name="cameraPermissionDeniedTitle">Nepodařilo se získat přístup k fotoaparátu</string>
<string name="cameraPermissionDeniedTitle">Nelze získat přístup k fotoaparátu</string>
<string name="importCards">Importovat karty</string>
<string name="updateBalance">Aktualizovat zůstatek</string>
<string name="currentBalanceSentence">Současný zůstatek: <xliff:g>%s</xliff:g></string>
@@ -232,8 +244,8 @@
<string name="switchToFrontImage">Přepnout na přední obrázek</string>
<string name="switchToBackImage">Přepnout na zadní obrázek</string>
<string name="switchToBarcode">Přepnout na čárový kód</string>
<string name="openFrontImageInGalleryApp">Otevřít přední obrázek v aplikaci prohlížeče obrázků</string>
<string name="openBackImageInGalleryApp">Otevřít zadní obrázek v aplikaci prohlížeče obrázků</string>
<string name="openFrontImageInGalleryApp">Otevřít přední obrázek v galerii</string>
<string name="openBackImageInGalleryApp">Otevřít zadní obrázek v galerii</string>
<string name="setBarcodeHeight">Nastavit výšku čárového kódu</string>
<string name="donate">Přispět</string>
<string name="icon_header_click_text">Dlouhým stisknutím miniaturu upravíte</string>
@@ -266,17 +278,18 @@
<string name="addWithoutBarcode">Přidat kartu bez čárového kódu</string>
<string name="field_must_not_be_empty">Položka nesmí být prázdná</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Vždy otáčet (ignoruje nastavení systému)</string>
<string name="continue_">Pokračovat</string>
<string name="add_manually_warning_title">Doporučuje se skenování</string>
<string name="add_manually_warning_message">U některých karet se hodnota čárového kódu liší od čísla napsaného na kartě. Z tohoto důvodu nemusí ruční zadání čárového kódu vždy fungovat. Doporučujeme místo toho naskenovat čárový kód pomocí fotoaparátu. Chcete přesto pokračovat?</string>
<string name="add_manually_warning_message">V některých obchodech se hodnota čárového kódu liší od čísla napsaného na kartě. Z tohoto důvodu nemusí ruční zadání čárového kódu vždy fungovat. Důrazně doporučujeme místo toho naskenovat čárový kód pomocí fotoaparátu. Chcete přesto pokračovat?</string>
<string name="spend">Utratit</string>
<string name="receive">Obdržet</string>
<string name="amountParsingFailed">Neplatné množství</string>
<string name="addFromPdfFile">Vybrat soubor PDF</string>
<string name="errorReadingFile">Soubor se nepodařilo přečíst</string>
<string name="errorReadingFile">Soubor nelze přečíst</string>
<string name="pageWithNumber">Stránka <xliff:g>%d</xliff:g></string>
<string name="multipleBarcodesFoundPleaseChooseOne">Který z nalezených čárových kódů chcete použít?</string>
<string name="failedLaunchingFileManager">Nepodařilo se nat podporovaného správce souborů</string>
<string name="failedLaunchingFileManager">Nelze nalézt podporovaný správce souborů</string>
<string name="noCameraFoundGuideText">Zdá se, že vaše zařízení nemá fotoaparát. Pokud ano, zkuste zařízení restartovat. V opačném případě použijte tlačítko Další možnosti a přidejte čárový kód jiným způsobem.</string>
<string name="importCancelled">Import zrušen</string>
<string name="exportCancelled">Export zrušen</string>
@@ -284,10 +297,10 @@
<string name="useFrontImage">Použít přední obrázek</string>
<string name="settings_use_volume_keys_navigation_summary">Pomocí tlačítek hlasitosti můžete změnit, která karta se zobrazí</string>
<string name="settings_use_volume_keys_navigation">Přepínat karty pomocí tlačítek hlasitosti</string>
<string name="generic_error_please_retry">Došlo k chybě</string>
<string name="generic_error_please_retry">Je nám líto, něco se pokazilo, zkuste to prosím znovu...</string>
<string name="settings_column_count_portrait">Sloupce v režimu na výšku</string>
<string name="settings_automatic_column_count">Automatický</string>
<string name="addFromPkpass">Vyberte soubor Passbook (.pkpass / .pkpasses)</string>
<string name="addFromPkpass">Vyberte soubor Passbook (.pkpass)</string>
<string name="unsupportedFile">Tento soubor není podporován</string>
<string name="settings_category_title_cards_overview">Přehled karet</string>
<string name="settings_column_count_landscape">Sloupce v režimu na šířku</string>
@@ -303,12 +316,4 @@
<string name="width">Šířka</string>
<string name="card_list_widget_name">Seznam karet</string>
<string name="card_list_widget_empty">Karty přidané do aplikace Catima se zobrazí zde. Pokud máte karty, ujistěte se, že nejsou všechny archivovány.</string>
<string name="cardWithNumber">Karta <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Karta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Neotáčejte prosím zařízení, protože tím zrušíte akci</string>
<string name="acra_catima_has_crashed">Omlouváme se, aplikace <xliff:g id="app_name">%s</xliff:g> havarovala. Pomozte nám prosím s opravou tohoto problému odesláním hlášení o chybě.</string>
<string name="acra_explain_crash">Pokud je to možné, přidejte prosím další podrobnosti o tom, co jste tu dělali:</string>
<string name="acra_crash_email_subject">Hlášení o pádu <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ptát se na odesílání hlášení o pádech</string>
<string name="pref_enable_acra_summary">Pokud je povoleno, budete při pádu aplikace dotázáni na jeho nahlášení. Hlášení nejsou nikdy odesílána automaticky.</string>
</resources>

View File

@@ -3,15 +3,15 @@
<string name="scanCardBarcode">Scan stregkode</string>
<string name="addCardTitle">Tilføj kort</string>
<string name="editCardTitle">Rediger kort</string>
<string name="sendLabel">Send…</string>
<string name="share">Del</string>
<string name="sendLabel">Afsend…</string>
<string name="share">Aktie</string>
<string name="ok">OK</string>
<string name="deleteConfirmation">Slet dette kort permanent?</string>
<string name="deleteConfirmation">Slete dette kort permanent\?</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Slet <xliff:g>%d</xliff:g> kort</item>
<item quantity="other">Slet <xliff:g>%d</xliff:g> korts</item>
<item quantity="one">Streichen <xliff:g>%d</xliff:g> kort</item>
<item quantity="other">Streichen <xliff:g>%d</xliff:g> korts</item>
</plurals>
<string name="deleteTitle">Slet kort</string>
<string name="deleteTitle">Karte streichen</string>
<string name="confirm">Bekræft</string>
<string name="delete">Slet</string>
<string name="edit">Rediger</string>
@@ -34,7 +34,7 @@
<string name="action_search">Søg</string>
<string name="importExport">Import/eksport</string>
<string name="exportName">Eksport</string>
<string name="importExportHelp">Sikkerhedskopiering af dine data, giver dig mulighed for at flytte dem til en anden enhed.</string>
<string name="importExportHelp">Sikkerhedskopiering af dit data, giver dig mulighed for at flytte dem til en anden enhed.</string>
<string name="importSuccessfulTitle">Importeret</string>
<string name="importFailedTitle">Import mislykkedes</string>
<string name="importFailed">Kunne ikke udføre importering</string>
@@ -54,12 +54,12 @@
\ncopyright © 2016-2020 Branden Archer.</string>
<string name="about">Om</string>
<string name="noCardsMessage">Tilføj først et kort</string>
<string name="cardShortcut">Genvej til kort</string>
<string name="cardShortcut">Kort genvej</string>
<string name="importOptionFilesystemButton">Fra filsystemet</string>
<string name="importOptionFilesystemExplanation">Vælg en bestemt fil fra filsystemet.</string>
<string name="importOptionFilesystemTitle">Import fra filsystem</string>
<string name="exportOptionExplanation">Dataene skrives til en placering efter eget valg.</string>
<string name="failedParsingImportUriError">Kunne ikke importere URI\'en</string>
<string name="failedParsingImportUriError">Kunne ikke analysere import-URI\'en</string>
<string name="noCardExistsError">Kunne ikke finde det kort</string>
<string name="deleteConfirmationGroup">Slet gruppe\?</string>
<string name="all">Alle</string>
@@ -79,16 +79,16 @@
<string name="moveDown">Bevæger sig nedad</string>
<string name="leaveWithoutSaveTitle">Afslut</string>
<string name="addManually">Indtast stregkoden manuelt</string>
<string name="noGiftCardsGroup">Opret kort og tildel dem grupper her.</string>
<string name="noGiftCardsGroup">Opret kort og tildel dem gupper her.</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Slet dette <xliff:g>%d</xliff:g> kort permanent\?</item>
<item quantity="other">Slet disse <xliff:g>%d</xliff:g> kort permanent\?</item>
</plurals>
<string name="app_name">Catima</string>
<string name="cameraPermissionRequired">Behov for kamera adgang er krævet for denne funktion…</string>
<string name="storageReadPermissionRequired">Behov for lager adgang er krævet for denne funktion…</string>
<string name="cameraPermissionRequired">Behov for kamera adgang krævet for denne funktion…</string>
<string name="storageReadPermissionRequired">Behov for lager adgang krævet for denne funktion…</string>
<string name="permissionReadCardsLabel">Læs Catima Kort</string>
<string name="permissionReadCardsDescription">læs dit Catima kort og alle kortets detaljer, også noter og billeder</string>
<string name="permissionReadCardsDescription">læs dine Catima kort og alle deres detaljer, også noter og billeder</string>
<string name="cameraPermissionDeniedTitle">Kunne ikke få adgang til kamera</string>
<string name="noCameraPermissionDirectToSystemSetting">For at scanne stregkoder, har Catima behov for at få adgang til dit kamera. Klik her for at ændre dine tilladelser i indstillinger.</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os og hjælpere</string>
@@ -100,6 +100,10 @@
<string name="group_name_already_in_use">Gruppenavn allerede i brug</string>
<string name="editGroup">Redigerer Gruppe: <xliff:g>%s</xliff:g></string>
<string name="importFidme">Importer fra FidMe</string>
<string name="settings_card_orientation">Skærm orientation</string>
<string name="settings_follow_system_orientation">Følg system</string>
<string name="settings_portrait_orientation">Portræt</string>
<string name="settings_landscape_orientation">Landskab</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deaktiver låseskærm når et kort er åbent</string>
<string name="groupsList">Grupper: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Udløber: <xliff:g>%s</xliff:g></string>
@@ -110,6 +114,7 @@
<string name="never">Aldrig</string>
<string name="chooseExpiryDate">Vælg udløbsdato</string>
<string name="balance">Balance</string>
<string name="importStocard">Importer fra Stocard</string>
<string name="balanceSentence">Balance: <xliff:g>%s</xliff:g></string>
<string name="group_name_is_empty">Gruppenavn må ikke være tom</string>
<string name="group_updated">Gruppe opdateret</string>
@@ -129,8 +134,10 @@
<string name="setBarcodeId">Vælg stregkode værdi</string>
<string name="sameAsCardId">Samme som ID</string>
<string name="settings_system_theme">System</string>
<string name="settings_lock_on_opening_orientation">Lås til orientation når kort åbnes</string>
<string name="settings_keep_screen_on_summary">Deaktiver skærm tids slukning når et kort er åbent</string>
<string name="group_edit">Rediger gruppe</string>
<string name="settings_follow_sensor_orientation">Altid roter (ignorer system indstillinger)</string>
<string name="chooseImportType">Importer data fra</string>
<string name="importVoucherVault">Importer fra Voucher Vault</string>
<string name="settings_use_volume_keys_navigation">Skift kort ved brug af lydstyrke knapperne</string>
@@ -144,4 +151,4 @@
<item quantity="one"><xliff:g>%s</xliff:g> point</item>
<item quantity="other"><xliff:g>%s</xliff:g> point</item>
</plurals>
</resources>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_search">Suche</string>
<string name="action_search">Suchen</string>
<string name="action_add">Hinzufügen</string>
<string name="noGiftCards">Füge eine Karte mit + hinzu oder importiere welche über das ⋮ Menü</string>
<string name="noGiftCards">Füge eine Karte mit + hinzu oder importiere welche über das ⋮ Menü.</string>
<string name="noMatchingGiftCards">Keine Ergebnisse. Versuche, deine Suche zu ändern.</string>
<string name="storeName">Name</string>
<string name="note">Notiz</string>
@@ -24,7 +24,7 @@
<string name="noCardExistsError">Konnte die Karte nicht finden</string>
<string name="importExport">Import/Export</string>
<string name="exportName">Export</string>
<string name="importExportHelp">Wenn du deine Daten sicherst, kannst du sie auf ein anderes Gerät übertragen</string>
<string name="importExportHelp">Wenn du deine Daten sicherst, kannst du sie auf ein anderes Gerät übertragen.</string>
<string name="importSuccessfulTitle">Importiert</string>
<string name="importFailedTitle">Import fehlgeschlagen</string>
<string name="importFailed">Import konnte nicht durchgeführt werden</string>
@@ -34,7 +34,7 @@
<string name="importing">Importiere…</string>
<string name="exporting">Exportiere…</string>
<string name="importOptionFilesystemTitle">Aus Dateisystem importieren</string>
<string name="importOptionFilesystemExplanation">Wähle eine bestimmte Datei aus dem Dateisystem aus</string>
<string name="importOptionFilesystemExplanation">Wähle eine bestimmte Datei aus dem Dateisystem aus.</string>
<string name="importOptionFilesystemButton">vom Dateisystem</string>
<string name="about">Über</string>
<string name="app_license">Freie Software, lizensiert unter der GPLv3+</string>
@@ -53,20 +53,20 @@
<string name="settings_theme">Farbschema</string>
<string name="app_copyright_old">Basierend auf Loyalty Card Keychain
\nCopyright © 2016-2020 Branden Archer</string>
<string name="exportOptionExplanation">Die Daten werden an einen Ort deiner Wahl geschrieben</string>
<string name="exportOptionExplanation">Die Daten werden an einen Ort deiner Wahl geschrieben.</string>
<string name="failedParsingImportUriError">Die Import-URI konnte nicht verarbeitet werden</string>
<string name="share">Teilen</string>
<string name="barcodeType">Barcodetyp</string>
<string name="starImage">Favoritenstern</string>
<string name="deleteConfirmationGroup">Gruppe löschen?</string>
<string name="all">Alle</string>
<string name="noGroups">Klicke auf das Pluszeichen +, um eine Gruppe hinzuzufügen</string>
<string name="noGroups">Klicke auf das Pluszeichen +, um eine Gruppe hinzuzufügen.</string>
<string name="noGroupCards">Diese Gruppe ist leer</string>
<string name="groups">Gruppen</string>
<string name="enter_group_name">Gruppennamen eingeben</string>
<string name="leaveWithoutSaveConfirmation">Beenden ohne zu speichern\?</string>
<string name="leaveWithoutSaveTitle">Beenden</string>
<string name="failedOpeningFileManager">Dateimanager konnte nicht geöffnet werden</string>
<string name="failedOpeningFileManager">Installiere zuerst einen Dateimanager.</string>
<string name="noBarcode">Kein Barcode</string>
<string name="addManually">Barcode manuell eingeben</string>
<string name="moveDown">Nach unten verschieben</string>
@@ -94,13 +94,14 @@
<string name="settings_keep_screen_on">Bildschirm aktiv lassen</string>
<string name="accept">Annehmen</string>
<string name="privacy_policy">Datenschutzrichtlinie</string>
<string name="importVoucherVaultMessage">Wähle deinen Export aus Voucher Vault zum Importieren aus.\nErstelle ihn, indem du Export in Voucher Vault drückst.</string>
<string name="importVoucherVaultMessage">Wähle deinen <i>vouchervault.json</i>-Export aus Voucher Vault zum Importieren aus. \nErstelle ihn, indem du zuerst auf Export in Voucher Vault drückst.</string>
<string name="importVoucherVault">Aus Voucher Vault importieren</string>
<string name="importLoyaltyCardKeychainMessage">Wähle deinen Export vom „Loyalty Card Keychain“ zum Importieren aus.\nErstelle ihn über das Import/Export Menü in Loyalty Card Keychain, indem du dort auf Export drückst.</string>
<string name="importLoyaltyCardKeychainMessage">Wählen du deinen <i>LoyaltyCardKeychain.csv</i>-Export aus Loyalty Card Keychain zum Importieren aus.
\nErstelle ihn über das Menü Import/Export in Loyalty Card Keychain, indem du dort zuerst auf Export drückst.</string>
<string name="importLoyaltyCardKeychain">Aus Loyalty Card Keychain importieren</string>
<string name="importFidmeMessage">Wähle deinen „FidMe-Export“ zum Importieren und anschließend manuell die Barcodetypen aus.\nErstelle ihn aus deinem FidMe-Profil, indem du Datenschutz wählst und dann auf Meine Daten extrahieren drückst.</string>
<string name="importFidmeMessage">Wähle deinen <i>fidme-export-request-xxxxxx.zip</i>-Export aus FidMe zum Importieren aus und wähle anschließend die Barcodetypen manuell aus. \nOder erstelle ihn aus deinem FidMe-Profil, indem du Datenschutz wählst und dann zuerst auf Meine Daten extrahieren drückst.</string>
<string name="importFidme">Aus FidMe importieren</string>
<string name="importCatimaMessage">Wähle deinen „Catima-Export“ zum Importieren aus.\nErstelle diesen durch das Drücken auf Export im Import/Export-Menü in einer anderen Catima-Anwendung.</string>
<string name="importCatimaMessage">Wähle deinen „<i>catima.zip</i>-Export“ von Catima zum Importieren aus.\nErstelle ihn zuerst aus dem Import/Export-Menü einer anderen Catima-Anwendung, indem du dort Export drückst.</string>
<string name="importCatima">Aus Catima importieren</string>
<string name="setBarcodeId">Barcodewert festlegen</string>
<string name="sameAsCardId">Entspricht Kartennummer</string>
@@ -109,9 +110,9 @@
<string name="noBarcodeFound">Keinen Barcode erkannt</string>
<string name="addFromImage">Bild aus der Galerie wählen</string>
<string name="unsupportedBarcodeType">Dieser Barcodetyp kann noch nicht angezeigt werden. Wir hoffen das Format in einer zukünftigen Version zu unterstützen.</string>
<string name="wrongValueForBarcodeType">Der Wert ist ungültig für den gewählten Barcodetyp</string>
<string name="app_resources">Ressourcen von Drittanbietern: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotheken von Drittanbietern: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="wrongValueForBarcodeType">Der Wert ist für den gewählten Barcodetyp leider nicht gültig</string>
<string name="app_resources">Freie Ressourcen von Drittanbietern: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Freie Bibliotheken von Drittanbietern: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="intent_import_card_from_url_share_multiple_text">Ich möchte diese Karten mit dir teilen</string>
<string name="no">Nein</string>
<string name="yes">Ja</string>
@@ -124,10 +125,12 @@
<string name="photos">Fotos</string>
<string name="frontImageDescription">Vorderseite</string>
<string name="backImageDescription">Rückseite</string>
<string name="passwordRequired">Gib das Passwort ein</string>
<string name="passwordRequired">Bitte gib das Passwort ein</string>
<string name="importStocardMessage">Wähle deinen <i>***.zip</i>-Export aus Stocard zum Importieren aus. \nDu erhälst ihn, indem du eine E-Mail an support@stocardapp.com sendest und um einen Export deiner Daten bittest.</string>
<string name="importStocard">Von Stocard importieren</string>
<string name="turn_flashlight_off">Blitzlicht ausschalten</string>
<string name="turn_flashlight_on">Blitzlicht einschalten</string>
<string name="failedGeneratingShareURL">Teilbare URL konnte nicht erstellt werden</string>
<string name="failedGeneratingShareURL">URL konnte nicht erstellt werden. Bitte melde das an uns.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> ausgewählt</item>
<item quantity="other"><xliff:g>%d</xliff:g> ausgewählt</item>
@@ -176,9 +179,9 @@
<string name="group_name_already_in_use">Der Gruppenname wird bereits verwendet</string>
<string name="group_name_is_empty">Gruppenname darf nicht leer sein</string>
<string name="group_updated">Gruppe aktualisiert</string>
<string name="editGroup">Gruppe bearbeiten: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Gruppe wird bearbeitet: <xliff:g>%s</xliff:g></string>
<string name="group_edit">Gruppe bearbeiten</string>
<string name="noGiftCardsGroup">Erstelle einige Karten und ordne sie dann hier der Gruppe zu</string>
<string name="noGiftCardsGroup">Erstelle einige Karten und ordne sie dann hier der Gruppe zu.</string>
<string name="setIcon">Vorschaubild festlegen</string>
<string name="selectColor">Farbe auswählen</string>
<string name="translate_platform">auf Weblate</string>
@@ -192,16 +195,21 @@
</plurals>
<string name="settings_oled_dark">Komplett schwarzer Hintergrund im dunklen Design</string>
<string name="include_if_asking_support">Wenn Du Unterstützung haben möchtest, gib bitte folgende Informationen an:</string>
<string name="settings_follow_system_orientation">System folgen</string>
<string name="settings_landscape_orientation">Querformat</string>
<string name="settings_portrait_orientation">Hochformat</string>
<string name="duplicateCard">Duplizieren</string>
<string name="unarchive">Aus dem Archiv wiederherstellen</string>
<string name="settings_card_orientation">Bildschirm-Ausrichtung</string>
<string name="unarchived">Karte aus dem Archiv wiederhergestellt</string>
<string name="archive">Archivieren</string>
<string name="archived">Karte archiviert</string>
<string name="settings_lock_on_opening_orientation">Kartenausrichtung nach dem Öffnen beibehalten</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> Karte (<xliff:g id="archivedCount">%2$d</xliff:g> archiviert)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> Karten (<xliff:g id="archivedCount">%2$d</xliff:g> archiviert)</item>
</plurals>
<string name="failedLaunchingPhotoPicker">Keine unterstützte App zur Bildauswahl gefunden</string>
<string name="failedLaunchingPhotoPicker">Es konnte keine unterstützte Galerie-App gefunden werden</string>
<string name="previousCard">Vorherige</string>
<string name="nextCard">Nächste</string>
<string name="failedToOpenUrl">Bitte installiere zuerst einen Webbrowser</string>
@@ -209,7 +217,7 @@
<string name="barcodeLongPressMessage">In der Galerie können nur Bilder geöffnet werden</string>
<string name="failedToRetrieveImageFile">Bilddatei konnte nicht abgerufen werden</string>
<string name="updateBalanceTitle">Wie viel hast du ausgegeben oder erhalten?</string>
<string name="cameraPermissionDeniedTitle">Kein Zugriff auf die Kamera</string>
<string name="cameraPermissionDeniedTitle">Kein Zugriff auf die Kamera möglich</string>
<string name="noCameraPermissionDirectToSystemSetting">Um Barcodes zu scannen, benötigt Catima Zugriff auf deine Kamera. Tippe hier, um deine Berechtigungseinstellungen zu ändern.</string>
<string name="updateBalanceHint">Betrag eingeben</string>
<string name="importCards">Karten importieren</string>
@@ -224,8 +232,8 @@
<string name="anyDate">Beliebiges Datum</string>
<string name="icon_header_click_text">Zum Bearbeiten des Vorschaubildes lang drücken</string>
<string name="switchToBarcode">Zum Barcode wechseln</string>
<string name="openFrontImageInGalleryApp">Vorderseite in Bildbetrachter öffnen</string>
<string name="openBackImageInGalleryApp">Rückseite in Bildbetrachter öffnen</string>
<string name="openFrontImageInGalleryApp">Vorderseite in Galerie öffnen</string>
<string name="openBackImageInGalleryApp">Rückseite in Galerie öffnen</string>
<string name="height">Höhe</string>
<string name="switchToFrontImage">Zur Vorderseite wechseln</string>
<string name="switchToBackImage">Zur Rückseite wechseln</string>
@@ -260,9 +268,10 @@
<string name="field_must_not_be_empty">Feld darf nicht leer sein</string>
<string name="manually_enter_barcode_instructions">Trage die Kartenummer oder Text deiner Karte ein und drücke auf den Barcode, der wie der auf deiner Karte aussieht.</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Immer drehen (ignoriert Systemeinstellungen)</string>
<string name="continue_">Fortfahren</string>
<string name="add_manually_warning_title">Scannen empfohlen</string>
<string name="add_manually_warning_message">In einigen Geschäften weicht der Wert des Barcodes von dem auf der Karte angegebenen Wert ab. Aus diesem Grund funktioniert die manuelle Eingabe des Barcodes in einigen Fällen nicht. Es wird empfohlen, stattdessen den Barcode mit deiner Kamera zu scannen. Möchtest du dennoch fortfahren?</string>
<string name="add_manually_warning_message">In einigen Geschäften weicht der Wert des Barcodes von dem auf der Karte angegebenen Wert ab. Aus diesem Grund funktioniert die manuelle Eingabe des Barcodes in einigen Fällen nicht. Es wird dringend empfohlen, den Barcode mit einer Kamera zu scannen. Möchtest du dennoch fortfahren?</string>
<string name="spend">Zahlen</string>
<string name="receive">Erhalten</string>
<string name="amountParsingFailed">Ungültiger Betrag</string>
@@ -289,20 +298,12 @@
<string name="settings_column_count_4">4</string>
<string name="settings_column_count_5">5</string>
<string name="settings_column_count_6">6</string>
<string name="generic_error_please_retry">Ein Fehler ist aufgetreten</string>
<string name="generic_error_please_retry">Entschuldigung, da ist etwas schief gelaufen, versuchen Sie es noch einmal ...</string>
<string name="unsupportedFile">Diese Datei wird nicht unterstützt</string>
<string name="addFromPkpass">Eine Passbook-Datei (.pkpass / .pkpasses) auswählen</string>
<string name="addFromPkpass">Passbook-Datei (.pkpass) auswählen</string>
<string name="sort_by_valid_from">Gültig ab</string>
<string name="width">Breite</string>
<string name="setBarcodeWidth">Barcodebreite einstellen</string>
<string name="card_list_widget_empty">Nachdem du einige Treuekarten in Catima hinzugefügt hast, werden sie hier angezeigt. Wenn du Karten hast, stelle sicher, dass diese nicht alle archiviert sind.</string>
<string name="card_list_widget_empty">Nachdem du einige Treuekarten in Catima hinzugefügt hast, werden sie hier angezeigt. Wenn du Karten hast, stelle sicher, dass sie nicht alle archiviert sind.</string>
<string name="card_list_widget_name">Kartenliste</string>
<string name="cardWithNumberAndLocale">Karte <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="cardWithNumber">Karte <xliff:g>%d</xliff:g></string>
<string name="pref_enable_acra_summary">Wenn aktiviert, wirst du bei einem Absturz gebeten diesen zu melden. Absturzberichte werden niemals automatisch gesendet.</string>
<string name="pref_enable_acra">Bitte um die Übermittlung von Absturzberichten</string>
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> Absturzbericht</string>
<string name="acra_explain_crash">Wenn möglich, bitte übermittle mehr Details zu dem, was du hier getan hast:</string>
<string name="acra_catima_has_crashed">Es tut uns leid, aber <xliff:g id="app_name">%s</xliff:g> ist abgestürzt. Bitte hilf uns diesen Fehler zu beheben und übermittle uns einen Absturzbericht.</string>
<string name="pleaseDoNotRotateTheDevice">Bitte drehe nicht das Gerät, weil sonst die Aktion abbricht</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Προσθήκη</string>
<string name="noGiftCards">Κάντε κλικ στο + κουμπί για να προσθέσετε μία κάρτα ή προσθέστε από το ⋮ μενού</string>
<string name="noGiftCards">Κάντε κλικ στο + κουμπί για να προσθέσετε μία κάρτα ή προσθέστε από το ⋮ μενού.</string>
<string name="storeName">Όνομα</string>
<string name="note">Σημείωση</string>
<string name="cardId">Κωδικός κάρτας</string>
@@ -12,15 +12,15 @@
<string name="confirm">Επιβεβαίωση</string>
<string name="ok">Εντάξει</string>
<string name="sendLabel">Αποστολή…</string>
<string name="editCardTitle">Επεξεργασία κάρτας</string>
<string name="addCardTitle">Προσθήκη κάρτας</string>
<string name="scanCardBarcode">Σάρωση γραμμωτού κώδικα</string>
<string name="cardShortcut">Συντόμευση κάρτας</string>
<string name="editCardTitle">Επεξεργασία Κάρτας</string>
<string name="addCardTitle">Προσθήκη Κάρτας</string>
<string name="scanCardBarcode">Σαρώστε τον γραμμωτό κώδικα</string>
<string name="cardShortcut">Συντόμευση Κάρτας</string>
<string name="noCardsMessage">Προσθέστε μία κάρτα πρώτα</string>
<string name="noCardExistsError">Δεν ήταν δυνατό να εντοπιστεί η κάρτα</string>
<string name="importExport">Εισαγωγή/Εξαγωγή</string>
<string name="exportName">Εξαγωγή</string>
<string name="importExportHelp">Τα αντίγραφα ασφαλείας σας επιτρέπουν να τα εισάγετε σε άλλη συσκευή</string>
<string name="importExportHelp">Τα αντίγραφα ασφαλείας σας επιτρέπουν να τα εισάγετε σε άλλη συσκευή.</string>
<string name="importSuccessfulTitle">Εισήχθησαν</string>
<string name="importFailedTitle">Εισαγωγή ανεπιτυχής</string>
<string name="importFailed">Δεν ήταν δυνατή η εισαγωγή</string>
@@ -30,7 +30,7 @@
<string name="importing">Γίνεται εισαγωγή του…</string>
<string name="exporting">Γίνεται εξαγωγή του…</string>
<string name="importOptionFilesystemTitle">Εισαγωγή από το σύστημα αρχείων</string>
<string name="importOptionFilesystemExplanation">Επιλέξτε ένα συγκεκριμένο αρχείο από το σύστημα αρχείων</string>
<string name="importOptionFilesystemExplanation">Επιλέξτε ένα συγκεκριμένο αρχείο από το σύστημα αρχείων.</string>
<string name="importOptionFilesystemButton">Από το σύστημα αρχείων</string>
<string name="about">Σχετικά</string>
<string name="app_license">Άδεια χρήσης υπό GPLv3+</string>
@@ -50,7 +50,7 @@
<item quantity="one"><xliff:g>%d</xliff:g> επιλέγχθηκε</item>
<item quantity="other"><xliff:g>%d</xliff:g> επιλέγχθηκαν</item>
</plurals>
<string name="noGiftCardsGroup">Δημιούργησε κάρτες και βάλτες σε αυτή την ομάδα</string>
<string name="noGiftCardsGroup">Δημιούργησε κάρτες και βάλτες σε αυτή την ομάδα.</string>
<string name="addManually">Εισαγωγή γραμμωτού κώδικα με μη αυτόματο τρόπο</string>
<string name="never">Ποτέ</string>
<string name="share">Κοινοποίηση</string>
@@ -58,7 +58,7 @@
<item quantity="one"><xliff:g>%s</xliff:g> πόντος</item>
<item quantity="other"><xliff:g>%s</xliff:g> πόντοι</item>
</plurals>
<string name="exportOptionExplanation">Τα δεδομένα θα μεταφερθούν σε τοποθεσία της επιλογής σας</string>
<string name="exportOptionExplanation">Τα δεδομένα θα μεταφερθούν σε τοποθεσία της επιλογής σας.</string>
<string name="settings_theme">Θέμα</string>
<string name="groupsList">Ομάδες: <xliff:g>%s</xliff:g></string>
<string name="barcodeId">Τιμή γραμμωτού κώδικα</string>
@@ -81,13 +81,14 @@
<string name="source_repository">Αποθήκη κώδικα</string>
<string name="on_github">στο GitHub</string>
<string name="on_google_play">στο Google Play</string>
<string name="report_error">Αναφορά σφάλματος</string>
<string name="report_error">Αναφορά Σφάλματος</string>
<string name="starred">Αγαπημένα</string>
<string name="translate_platform">στο Weblate</string>
<string name="importLoyaltyCardKeychain">Εισαγωγή από Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">Επιλέξτε την εξαγωγή σας από το Loyalty Card Keychain για εισαγωγή. \nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής στο Loyalty Card Keychain επιλέγοντας Εξαγωγή.</string>
<string name="importLoyaltyCardKeychainMessage">Επιλέξτε την <i>LoyaltyCardKeychain.csv</i> εξαγωγή από το Loyalty Card Keychain για εισαγωγή.
\nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής στο Loyalty Card Keychain επιλέγοντας Εξαγωγή.</string>
<string name="importFidme">Εισαγωγή από FidMe</string>
<string name="importFidmeMessage">Επιλέξτε την εξαγωγή σας από το FidMe για εισαγωγή και επιλέξτε χειροκίνητα τους τύπους γραμμωτού κώδικα.\nΔημιουργήστε το από το FidMe προφίλ επιλέγοντας Προστασία Δεδομένων και πατώντας Εξαγωγή δεδομένων.</string>
<string name="importFidmeMessage">Επιλέξτε την <i>fidme-export-request-xxxxxx.zip</i> εξαγωγή από το FidMe για εισαγωγή και επιλέξτε χειροκίνητα τους τύπους γραμμωτού κώδικα μετέπειτα.\nΔημιουργήστε το από το FidMe προφίλ επιλέγοντας Προστασία Δεδομένων και πατώντας Εξαγωγή δεδομένων πρώτα.</string>
<string name="setBarcodeId">Επιλέξτε τιμή γραμμωτού κώδικα</string>
<string name="wrongValueForBarcodeType">Η τιμή δεν είναι έγκυρη για τον επιλεγμένο γραμμωτό κώδικα</string>
<string name="setBackImage">Επιλογή οπίσθιας εικόνας</string>
@@ -105,7 +106,7 @@
<item quantity="one">Διαγραφή <xliff:g>%d</xliff:g> κάρτας</item>
<item quantity="other">Διαγραφή <xliff:g>%d</xliff:g> καρτών</item>
</plurals>
<string name="errorReadingImage">Δεν ήταν δυνατή η ανάγνωση της εικόνας</string>
<string name="errorReadingImage">Δεν ήταν δυνατό να διαβαστεί η εικόνα</string>
<string name="currency">Νόμισμα</string>
<string name="privacy_policy">Πολιτική απορρήτου</string>
<string name="chooseImportType">Εισαγωγή δεδομένων από</string>
@@ -114,27 +115,31 @@
<item quantity="one"><xliff:g>%1$d</xliff:g> κάρτα ( <xliff:g id="archivedCount">%2$d</xliff:g> αρχειοθετήθηκε)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> κάρτες ( <xliff:g id="archivedCount">%2$d</xliff:g> αρχειοθετήθηκαν)</item>
</plurals>
<string name="importCatimaMessage">Επιλέξτε την εξαγωγή σας από το Catima για εισαγωγή.\nΔημιουργήστε την από το μενού Εισαγωγή/Εξαγωγή μιας άλλης εφαρμογής Catima πατώντας Εξαγωγή.</string>
<string name="importCatimaMessage">Επιλέξτε την <i>catima.zip</i> εξαγωγή από το Catima για εισαγωγή
\nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής μιας άλλης εφαρμογής Catima κάνοντας εξαγωγή εκεί πρώτα.</string>
<string name="importStocardMessage">Επιλέξτε την <i>***.zip</i> εξαγωγή από το Stocard για εισαγωγή.
\nΠάρτε το στέλνοντας email στο: support@stocardapp.com ζητώντας μια εξαγωγή αρχείων των δεδομένων σας.</string>
<string name="intent_import_card_from_url_share_multiple_text">Θέλω να μοιραστώ μερικές κάρτες μαζί σου</string>
<string name="editGroup">Επεξεργασία ομάδας: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Επεξεργασία Ομάδας: <xliff:g>%s</xliff:g></string>
<string name="setFrontImage">Επιλογή εμπρόσθιας εικόνας</string>
<string name="importVoucherVaultMessage">Επιλέξτε την εξαγωγή σας από το Voucher Vault για εισαγωγή. \nΔημιουργήστε το επιλέγοντας Εξαγωγή στο Voucher Vault.</string>
<string name="importVoucherVaultMessage">Επιλέξτε την <i>vouchervault.json</i> εξαγωγή από το Voucher Vault για εισαγωγή.
\nΔημιουργήστε το επιλέγοντας Εξαγωγή στο Voucher Vault.</string>
<string name="unsupportedBarcodeType">Ο τύπος γραμμωτού κώδικα δεν μπορεί να εμφανιστεί ακόμα. Μπορεί να υποστηρίζεται σε μια μελλοντική έκδοση της εφαρμογής.</string>
<string name="frontImageDescription">Εμπρόσθια</string>
<string name="photos">Φωτογραφίες</string>
<string name="backImageDescription">Οπίσθια</string>
<string name="updateBarcodeQuestionTitle">Ενημέρωση τιμής γραμμωτού κώδικα;</string>
<string name="passwordRequired">Εισάγετε τον κωδικό</string>
<string name="sort_by_most_recently_used">Πρόσφατα χρησιμοποιημένα</string>
<string name="passwordRequired">Παρακαλώ εισάγετε τον κωδικό</string>
<string name="sort_by_most_recently_used">Χρήση</string>
<string name="shortcutSelectCard">Επιλέξτε μία κάρτα</string>
<string name="barcodeImageDescriptionWithType">Εικόνα <xliff:g>%s</xliff:g> γραμμωτού κώδικα</string>
<string name="app_libraries">Βιβλιοθήκες τρίτων: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Ελεύθερες βιβλιοθήκες τρίτων: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="license">Άδεια</string>
<string name="include_if_asking_support">Αν θέλετε να ζητήσετε υποστήριξη, συμπεριλάβετε τις ακόλουθες πληροφορίες:</string>
<string name="importSuccessful">Δεδομένα εισήχθησαν</string>
<string name="moveUp">Προχώρα πάνω</string>
<string name="barcodeType">Τύπος γραμμωτού κώδικα</string>
<string name="app_resources">Πηγές τρίτων: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_resources">Ελεύθερες πηγές τρίτων: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="selectColor">Επιλογή χρώματος</string>
<string name="setIcon">Ορισμός εικονιδίου</string>
<string name="settings_sky_blue_theme">Γαλάζιο</string>
@@ -148,7 +153,7 @@
<string name="points">Πόντοι</string>
<string name="exportSuccessful">Δεδομένα εξήχθησαν</string>
<string name="settings_disable_lockscreen_while_viewing_card">Αποτροπή κλειδώματος οθόνης</string>
<string name="failedLaunchingPhotoPicker">Δεν βρέθηκε υποστηριζόμενος επιλογέας εικόνων</string>
<string name="failedLaunchingPhotoPicker">Δεν βρέθηκε υποστηριζόμενη εφαρμογή συλλογής</string>
<string name="noBarcode">Χωρίς γραμμωτό κώδικα</string>
<string name="starImage">Αγαπημένο αστέρι</string>
<string name="balanceSentence">Υπόλοιπο: <xliff:g>%s</xliff:g></string>
@@ -159,15 +164,20 @@
</plurals>
<string name="app_copyright_old">Βασισμένο στο Loyalty Card Keychain
\nπνευματικά δικαιώματα © 2016-2020 Branden Archer</string>
<string name="settings_follow_system_orientation">Ακολούθηση συστήματος</string>
<string name="settings_card_orientation">Προσανατολισμός οθόνης</string>
<string name="settings_portrait_orientation">Πορτραίτο</string>
<string name="settings_landscape_orientation">Οριζόντια</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Πνευματικά δικαιώματα © 2019-<xliff:g>%d</xliff:g> Sylvia van Os</string>
<string name="settings_lock_on_opening_orientation">Κλείδωμα τρέχοντος προσανατολισμού όταν ανοίγει μία κάρτα</string>
<string name="intent_import_card_from_url_share_text">Θέλω να μοιραστώ μία κάρτα μαζί σου</string>
<string name="enter_group_name">Εισάγετε όνομα ομάδας</string>
<string name="groups">Ομάδες</string>
<string name="noGroups">Κάντε κλικ στο + κουμπί ώστε να προσθέσετε ομάδες για κατηγοριοποίηση</string>
<string name="noGroups">Κάντε κλικ στο + κουμπί ώστε να προσθέσετε ομάδες για κατηγοριοποίηση.</string>
<string name="group_name_already_in_use">Αυτό το όνομα υπάρχει ήδη</string>
<string name="group_updated">Η ομάδα ενημερώθηκε</string>
<string name="all">Όλες</string>
<string name="failedOpeningFileManager">Αποτυχία εκκίνησης διαχειριστή αρχείων</string>
<string name="failedOpeningFileManager">Εγκαταστήστε έναν διαχειριστή αρχείων πρώτα.</string>
<string name="leaveWithoutSaveConfirmation">Έξοδος χωρίς αποθήκευση;</string>
<string name="expiryStateSentenceExpired">Έληξε: <xliff:g>%s</xliff:g></string>
<string name="card">Κάρτα</string>
@@ -177,11 +187,12 @@
<string name="noBarcodeFound">Δεν βρέθηκε γραμμωτός κώδικα</string>
<string name="balance">Υπόλοιπο</string>
<string name="importCatima">Εισαγωγή από Catima</string>
<string name="importStocard">Εισαγωγή από Stocard</string>
<string name="importVoucherVault">Εισαγωγή από Voucher Vault</string>
<string name="sameAsCardId">Όπως ο κωδικός</string>
<string name="exportPassword">Προσθέστε έναν κωδικό για προστασία της εξαγωγής (προαιρετικά)</string>
<string name="exportPasswordHint">Εισαγωγή κωδικού</string>
<string name="failedGeneratingShareURL">Δεν ήταν δυνατή η δημιουργία κοινοποιούμενου URL</string>
<string name="failedGeneratingShareURL">Δεν ήταν δυνατή η δημιουργία κοινοποιούμενου URL. Παρακαλώ αναφέρετε το.</string>
<string name="turn_flashlight_on">Ενεργοποίηση φακού</string>
<string name="turn_flashlight_off">Απενεργοποίηση φακού</string>
<string name="settings_locale">Γλώσσα</string>
@@ -234,6 +245,7 @@
<string name="icon_header_click_text">Πατήστε παρατεταμένα για επεξεργασία του εικονιδίου</string>
<string name="openFrontImageInGalleryApp">Ανοίξτε την εμπρόσθια εικόνα στη συλλογή εικόνων</string>
<string name="storageReadPermissionRequired">Δικαίωμα ανάγνωσης του χώρου αποθήκευσης απαραίτητο για αυτήν την ενέργεια…</string>
<string name="settings_follow_sensor_orientation">Πάντα σε περιστροφή (αγνοεί τις ρυθμίσεις του συστήματος)</string>
<string name="validFromDate">Ισχύει από</string>
<string name="anyDate">Οποιαδήποτε ημερομηνία</string>
<string name="chooseValidFromDate">Επιλέξτε έγκυρη ημερομηνία από</string>
@@ -244,7 +256,7 @@
<string name="continue_">Συνέχεια</string>
<string name="settings_category_title_privacy">Απόρρητο</string>
<string name="addFromPdfFile">Επιλογή αρχείου PDF</string>
<string name="add_manually_warning_message">Για ορισμένες κάρτες, ο γραμμωτός κώδικας διαφέρει από τον αριθμό που αναγράφεται πάνω στην κάρτα. Εξαιτίας αυτού, η εισαγωγή γραμμωτού κώδικα χειροκίνητα ενδέχεται να μην λειτουργεί πάντα. Προτείνεται να σαρώσετε τον γραμμωτό κώδικα με χρήση της κάμερας. Επιθυμείτε να συνεχίσετε;</string>
<string name="add_manually_warning_message">Για ορισμένα καταστήματα, ο γραμμωτός κώδικας διαφέρει από τον αριθμό που αναγράφεται πάνω στην κάρτα. Εξαιτίας αυτού, η εισαγωγή γραμμωτού κώδικα χειροκίνητα ενδέχεται να μην λειτουργεί πάντα. Προτείνεται να σκανάρετε τον γραμμωτό κώδικα με χρήση της κάμερας. Επιθυμείτε να συνεχίσετε ;</string>
<string name="amountParsingFailed">Μη έγκυρο ποσό</string>
<string name="show_balance">Προβολή υπολοίπου</string>
<string name="action_display_options">Επιλογές εμφάνισης</string>
@@ -289,20 +301,12 @@
<string name="settings_column_count_2">2</string>
<string name="settings_column_count_6">6</string>
<string name="settings_column_count_7">7</string>
<string name="generic_error_please_retry">Συνέβη ένα σφάλμα</string>
<string name="generic_error_please_retry">Λυπούμαστε, κάτι πήγε στραβά, δοκιμάστε ξανά...</string>
<string name="unsupportedFile">Το αρχείο δεν υποστηρίζεται</string>
<string name="addFromPkpass">Επιλέξτε αρχείο Passbook (.pkpass / .pkpasses)</string>
<string name="sort_by_valid_from">Έγκυρα από</string>
<string name="addFromPkpass">Επιλογή αρχείου Passbook (.pkpass)</string>
<string name="sort_by_valid_from">Έναρξη ισχύος</string>
<string name="setBarcodeWidth">Ορισμός πλάτους γραμμωτού κώδικα</string>
<string name="width">Πλάτος</string>
<string name="card_list_widget_empty">Αφού προσθέσετε μερικές κάρτες επιβράβευσης στο Catima, θα εμφανιστούν εδώ. Εάν έχετε κάρτες, βεβαιωθείτε ότι δεν είναι όλες αρχειοθετημένες.</string>
<string name="card_list_widget_name">Λίστα καρτών</string>
<string name="cardWithNumber">Κάρτα <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Κάρτα <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Μην περιστρέφετε τη συσκευή, καθώς αυτό θα ακυρώσει την ενέργεια</string>
<string name="acra_catima_has_crashed">Λυπούμαστε, αλλά το <xliff:g id="app_name">%s</xliff:g> παρουσίασε σφάλμα. Βοηθήστε μας να διορθώσουμε αυτό το πρόβλημα, στέλνοντάς μας μια αναφορά σφάλματος.</string>
<string name="acra_explain_crash">Αν είναι δυνατόν, προσθέστε περισσότερες λεπτομέρειες σχετικά με το τι κάνατε εδώ:</string>
<string name="acra_crash_email_subject">Αναφορά σφάλματος <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ερώτηση για αποστολή αναφορών σφαλμάτων</string>
<string name="pref_enable_acra_summary">Όταν είναι ενεργοποιημένη, θα σας ζητηθεί να αναφέρετε ένα σφάλμα όταν συμβεί. Οι αναφορές σφάλματος δεν αποστέλλονται ποτέ αυτόματα.</string>
</resources>

View File

@@ -74,7 +74,7 @@
<string name="intent_import_card_from_url_share_text">Mi deziras dividi karto kun vi</string>
<string name="exportSuccessful">Datumoj eksportitaj</string>
<string name="noGroupCards">Ĉi tiu grupo estas malplena</string>
<string name="noGiftCards">Klavu la \"+\" butonon por aldoni karton, aŭ importu el la menuo \" ⋮\"</string>
<string name="noGiftCards">Klavu la \"+\" butonon por aldoni karton, aŭ importu el la menuo \" ⋮\".</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> elektita</item>
<item quantity="other"><xliff:g>%d</xliff:g> elektitaj</item>
@@ -213,6 +213,10 @@
<string name="cameraPermissionDeniedTitle">Fotilo neatingebla</string>
<string name="noCameraPermissionDirectToSystemSetting">Por skani strikodojn Catima bezonas atingorajton al via fotilo. Klaku ĉi tie por ŝanĝi viajn permesajn agordojn.</string>
<string name="app_copyright_short">Kopirajto © Sylvia van Os kaj kontribuantoj</string>
<string name="settings_card_orientation">Orientiĝo de strikodo</string>
<string name="settings_follow_system_orientation">Laŭ la sistemo</string>
<string name="settings_portrait_orientation">Vertikala</string>
<string name="settings_landscape_orientation">Horizontala</string>
<string name="settings_display_barcode_max_brightness_summary">Bezonata por ke iuj skaniloj funkciu</string>
<string name="unsupportedBarcodeType">Ne eblas montri ĉi tiun strikodspecon. Ĝi eble estos subtenata en posta versio de la apo.</string>
<string name="importVoucherVaultMessage">Elektu la <i>vouchervault.json</i> eksporton de Voucher Vault kiun vi volas importi.
@@ -229,6 +233,8 @@
<string name="settings_pink_theme">Rozkolora</string>
<string name="field_must_not_be_empty">Kampo devas ne esti malplena</string>
<string name="manually_enter_barcode_instructions">Entajpu la identigilon aŭ tekston sur via karto kaj premu la strikodon kiu aspektas kiel tiu sur via karto.</string>
<string name="importStocardMessage">Elektu la <i>***.zip</i> eksoporton de Stocard kiun vi volas importi.
\nAkiru ĝin sendante retpoŝton al support@stocardapp.com petante eksporton de viaj datumoj.</string>
<string name="turn_flashlight_off">Malŝalti poŝlampon</string>
<string name="add_manually_warning_title">Skani estas rekomendata</string>
<string name="continue_">Daŭrigi</string>
@@ -250,6 +256,8 @@
<string name="pageWithNumber">Paĝo <xliff:g>%d</xliff:g></string>
<string name="settings_system_locale">Sistemo</string>
<string name="app_resources">Liberaj triaj risurcoj: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_sensor_orientation">Ĉiam turni (ignori la agordojn de la sistemo)</string>
<string name="settings_lock_on_opening_orientation">Fiksi al la orientiĝo uzata dum malfermado de la karto</string>
<string name="importCatimaMessage">Elektu la <i>catima.zip</i> eksporton kiun vi volas importi.
\nKreu ĝin unue en la importi/eksporti menuo en alia Catima apo elektante \'eksporti\' tie.</string>
<string name="importFidme">Importi el FidMe</string>
@@ -264,6 +272,7 @@
<string name="updateBarcodeQuestionText">Vi ŝanĝis la identigon. Ĉu vi volas ankaŭ ĝisdatigi la strikodon por uzi la saman valoron?</string>
<string name="importLoyaltyCardKeychainMessage">Elektu la <i>LoyaltyCardKeychain.csv</i> eksporton de Loyalty Card Keychain kiun vi volas importi.
\nKreu ĝin unue de la \"Importi/eksporti\" menuo en Loyalty Card Keychain elektante \"eksporti\" tie.</string>
<string name="importStocard">Importi de Stocard</string>
<string name="importVoucherVault">Importi el Voucher Vault</string>
<string name="turn_flashlight_on">Enŝalti poŝlampon</string>
<string name="settings_locale">Lingvo</string>

View File

@@ -12,13 +12,13 @@
<string name="exportFailed">No se pudo exportar</string>
<string name="noBarcode">Sin código de barra</string>
<string name="edit">Editar</string>
<string name="noGiftCards">Pulsá el botón + para agregar una tarjeta de regalo, o importá una desde el menú</string>
<string name="noGiftCardsGroup">Crea tarjetas de regalo, y asignales un grupo</string>
<string name="noGiftCards">Pulsá el botón + para agregar una tarjeta de regalo, o importá una desde el menú.</string>
<string name="noGiftCardsGroup">Crea tarjetas de regalo, y asignales un grupo.</string>
<string name="note">Nota</string>
<string name="unstar">Borrar de favoritos</string>
<string name="importExport">Importar/Exportar</string>
<string name="exportName">Exportar</string>
<string name="importExportHelp">Crear una copia de seguridad de sus datos, permite moverlos hacia otro dispositivo</string>
<string name="importExportHelp">Crear una copia de seguridad de sus datos, permite moverlos hacia otro dispositivo.</string>
<string name="importing">Importando…</string>
<string name="exporting">Exportando…</string>
<string name="save">Guardar</string>
@@ -44,12 +44,12 @@
<plurals name="deleteCardsTitle">
<item quantity="one">Borrar <xliff:g>%d</xliff:g> tajeta</item>
<item quantity="many">Borrar <xliff:g>%d</xliff:g> tarjetas</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<plurals name="deleteCardsConfirmation">
<item quantity="one">¿Borrar esta<xliff:g>%d</xliff:g> tarjeta de forma permanente\?</item>
<item quantity="many">Borrar estas <xliff:g>%d</xliff:g> tarjetas de forma permanente\?</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="failedOpeningFileManager">Primero instale un administrador de archivos.</string>
<string name="intent_import_card_from_url_share_multiple_text">Quiero compartirte algunas tarjetas</string>
@@ -58,15 +58,18 @@
<string name="about_title_fmt">Acerca de <xliff:g id="app_name">%s</xliff:g></string>
<string name="editBarcode">Editar código de barras</string>
<string name="removeImage">Remover imágen</string>
<string name="settings_portrait_orientation">Vertical</string>
<string name="takePhoto">Tomar una foto</string>
<string name="cameraPermissionDeniedTitle">No se pudo acceder a la cámara</string>
<string name="wrongValueForBarcodeType">El valor no es válido para el tipo de código de barras seleccionado</string>
<string name="expiryDate">Fecha de vencimiento</string>
<string name="importStocard">Importar desde Stocard</string>
<string name="currency">Moneda</string>
<string name="group_edit">Editar grupo</string>
<string name="debug_version_fmt">Versión: <xliff:g id="version">%s</xliff:g></string>
<string name="backImageDescription">Imágen dorsal</string>
<string name="noCameraPermissionDirectToSystemSetting">Para escanear códigos de barra, Catima necesitará acceso a la cámara. Presione aquí para cambiar la configuración de sus permisos.</string>
<string name="settings_lock_on_opening_orientation">Bloquear a la orientación utilizada al abrir la tarjeta</string>
<string name="app_loyalty_card_keychain">Cartera para Tarjetas de Fidelización</string>
<string name="importOptionFilesystemTitle">Importar desde su sistema de archivos</string>
<string name="leaveWithoutSaveTitle">Salir</string>
@@ -78,10 +81,11 @@
<string name="settings_dark_theme">Oscuro</string>
<string name="importFidme">Importar desde FidMe</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deshabilita el bloqueo de pantalla mientras se ve una tarjeta</string>
<string name="exportOptionExplanation">Los datos serán escritos a una ubicación de tu elección</string>
<string name="exportOptionExplanation">Los datos serán escritos a una ubicación de tu elección.</string>
<string name="app_copyright_old">Basado en Loyalty Card Keychain
\ncopyright © 20162020 Branden Archer</string>
<string name="importVoucherVaultMessage">Seleccione su exportado desde Voucher Vault para importarlo.\nCréelo presionando la opción Exportar en Voucher Vault.</string>
<string name="importVoucherVaultMessage">Seleccione su <i>vouchervault.json</i> exportado desde Voucher Vault para importarlo.
\nPrimero créelo presionando la opción Exportar en Voucher Vault.</string>
<string name="chooseImportType">Importar datos desde</string>
<string name="frontImageDescription">Imágen frontal</string>
<string name="settings_system_theme">Sistema</string>
@@ -99,10 +103,11 @@
<string name="settings_keep_screen_on">Mantener la pantalla encendida</string>
<string name="setBarcodeId">Establecer valor del código de barras</string>
<string name="importCatima">Importar desde Catima</string>
<string name="settings_follow_system_orientation">Seguir el sistema</string>
<string name="intent_import_card_from_url_share_text">Quiero compartirte una tarjeta</string>
<string name="addFromImage">Seleccione una imágen desde la galería</string>
<string name="app_copyright_short">Copyright © Sylvia van Os y colaboradores</string>
<string name="importOptionFilesystemExplanation">Elija un archivo desde su sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elija un archivo desde su sistema de archivos.</string>
<string name="exportSuccessful">Datos exportados</string>
<string name="settings_allow_content_provider_read_summary">Las aplicaciones aún tendrán que pedir permiso para obtener acceso</string>
<string name="editGroup">Edición de grupo: <xliff:g>%s</xliff:g></string>
@@ -111,8 +116,11 @@
<string name="about">Acerca de</string>
<string name="sameAsCardId">Igual que el código</string>
<string name="importOptionFilesystemButton">Desde el sistema de archivos</string>
<string name="settings_landscape_orientation">Horizontal</string>
<string name="privacy_policy">Política de Privacidad</string>
<string name="enter_group_name">Ingrese el nombre del grupo</string>
<string name="importStocardMessage">Seleccione su <i>***.zip</i> exportado desde Stocard para importarlo.
\nObténgalo mandando un correo electrónico a support@stocardapp.com preguntando por una copia de tus datos.</string>
<string name="addManually">Ingresar el código de barras manualmente</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os y colaboradores</string>
<string name="importVoucherVault">Importar desde Voucher Vault</string>
@@ -123,7 +131,8 @@
<string name="balance">Balance</string>
<string name="cameraPermissionRequired">Se necesita permiso para acceder a la cámara para realizar esta acción…</string>
<string name="settings_allow_content_provider_read_title">Permitir que otras aplicaciones accedan a mis datos</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su exportado desde Loyalty Card Keychain para importarlo.\nCréelo desde el menu Importar/Exportar de Loyalty Card Keychain al presionar la opción Exportar.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su <i>LoyaltyCardKeychain.csv</i> exportado desde Loyalty Card Keychain para importarlo.
\nPrimero créelo desde el menu Importar/Exportar de Loyalty Card Keychain al presionar la opción Exportar.</string>
<string name="settings_light_theme">Claro</string>
<string name="moveDown">Mover hacia abajo</string>
<string name="importLoyaltyCardKeychain">Importar desde Loyalty Card Keychain</string>
@@ -141,27 +150,30 @@
<string name="settings">Configuración</string>
<string name="selectBarcodeTitle">Seleccione el código de barras</string>
<string name="importSuccessful">Datos importados</string>
<string name="app_libraries">Librerías externas: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Recursos externos: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_card_orientation">Orientación del código de barras</string>
<string name="app_libraries">Librerías externas libres: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Recursos externos libres: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_name">Catima</string>
<string name="accept">Aceptar</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> tarjeta</item>
<item quantity="many"><xliff:g>%d</xliff:g> tarjetas</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> seleccionado</item>
<item quantity="many"><xliff:g>%d</xliff:g> seleccionados</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="importCatimaMessage">Seleccione su exportado desde Catima para importarlo.\nCréelo desde el menu para Importar/Exportar de otra aplicación de Catima presionando la opción Exportar.</string>
<string name="importCatimaMessage">Seleccione su <i>catima.zip</i> exportado desde Catima para importarlo.
\nPrimero créelo desde el menu para Importar/Exportar de otra aplicación de Catima presionando la opción Exportar.</string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punto</item>
<item quantity="many"><xliff:g>%s</xliff:g> puntos</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="importFidmeMessage">Seleccione su exportado de FidMe para importarlo, y a continuación seleccione manualmente los tipos de código de barras.\nCréelo desde su perfil de FidMe al elegir la opción Protección de Datos y presionando Extraer mis datos.</string>
<string name="importFidmeMessage">Seleccione su <i>fidme-export-request-xxxxxx.zip</i> exportado de FidMe para importarlo, y a continuación seleccione manualmente los tipos de código de barras.
\nPrimero créelo desde su perfil de FidMe al elegir la opción Protección de Datos y presionando Extraer mis datos.</string>
<string name="updateBarcodeQuestionTitle">¿Actualizar el valor del código de barras\?</string>
<string name="settings_keep_screen_on_summary">Deshabilita el tiempo de espera de la pantalla mientras se ve una tarjeta</string>
<string name="thumbnailDescription">Miniatura</string>
@@ -192,18 +204,17 @@
<string name="settings_blue_theme">Azul</string>
<string name="app_contributors">Hecho posible por: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="barcodeLongPressMessage">Solo se puede abrir imágenes en la aplicación de galería</string>
<string name="settings_follow_sensor_orientation">Siempre rotar (ignora configuración del sistema)</string>
<string name="yes">Si</string>
<string name="no">No</string>
<string name="passwordRequired">Ingresa la contraseña</string>
<string name="failedGeneratingShareURL">No se pudo generar URL compartible</string>
<string name="passwordRequired">Por favor ingresa la contraseña</string>
<string name="failedGeneratingShareURL">No se pudo generar URL compartible. Por favor reporte esto.</string>
<string name="sort_by_name">Nombre</string>
<string name="sort_by">Ordenar por</string>
<string name="reverse">en orden inverso</string>
<string name="sort_by_most_recently_used">Más recientemente usado</string>
<string name="sort_by_most_recently_used">Más Recientemente Usado</string>
<string name="settings_use_volume_keys_navigation">Cambiar tarjetas usando los botones de volumen</string>
<string name="sort_by_valid_from">Válido Desde</string>
<string name="sort_by_expiry">Vencimiento</string>
<string name="settings_use_volume_keys_navigation_summary">Usá los botones de volumen para cambiar la tarjeta que se muestra</string>
<string name="cardWithNumberAndLocale">Tarjeta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="cardWithNumber">Tarjeta <xliff:g>%d</xliff:g></string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Añadir</string>
<string name="noGiftCards">Haz clic en el botón + para añadir una tarjeta, o importa desde el menú ⋮</string>
<string name="noGiftCards">Haz clic en el botón + para añadir una tarjeta, o importa desde el menú ⋮.</string>
<string name="storeName">Nombre</string>
<string name="note">Nota</string>
<string name="cardId">ID de tarjeta</string>
@@ -20,7 +20,7 @@
<string name="noCardExistsError">No se ha podido encontrar esa tarjeta</string>
<string name="importExport">Importar/Exportar</string>
<string name="exportName">Exportar</string>
<string name="importExportHelp">Respaldar tus datos permite trasladarlos a otro dispositivo</string>
<string name="importExportHelp">Respaldar tus datos permite trasladarlos a otro dispositivo.</string>
<string name="importSuccessfulTitle">Importado</string>
<string name="importFailedTitle">Falló la importación</string>
<string name="importFailed">No se ha podido realizar la importación</string>
@@ -30,7 +30,7 @@
<string name="importing">Importando…</string>
<string name="exporting">Exportando…</string>
<string name="importOptionFilesystemTitle">Importar desde el sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elegir un archivo concreto del sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elegir un archivo concreto del sistema de archivos.</string>
<string name="importOptionFilesystemButton">Desde el sistema de archivos</string>
<string name="about">Información</string>
<string name="app_license">Programa libre con «copyleft», disponible en virtud de la licencia GPLv3+</string>
@@ -47,13 +47,13 @@
<string name="settings_theme">Tema</string>
<string name="app_copyright_old">Basado en Loyalty Card Keychain
\nderechos de autor © 2016-2020 de Branden Archer</string>
<string name="exportOptionExplanation">Los datos se guardarán en la ubicación que elija</string>
<string name="exportOptionExplanation">Los datos se guardarán en la ubicación que elija.</string>
<string name="failedParsingImportUriError">No se pudo procesar el URI de importación</string>
<string name="share">Compartir</string>
<string name="barcodeType">Tipo de código de barras</string>
<string name="noMatchingGiftCards">Sin resultados. Intente cambiar su búsqueda.</string>
<string name="action_search">Buscar</string>
<string name="noGroups">Pulse en el botón + para añadir grupos de categorización</string>
<string name="noGroups">Pulse en el botón + para añadir grupos de categorización.</string>
<string name="starImage">Estrella favorita</string>
<string name="thumbnailDescription">Miniatura</string>
<string name="selectBarcodeTitle">Seleccionar el código de barras</string>
@@ -67,7 +67,7 @@
<string name="leaveWithoutSaveTitle">Salir</string>
<string name="moveDown">Bajar</string>
<string name="moveUp">Subir</string>
<string name="failedOpeningFileManager">No se puedo abrir un gestor de archivos</string>
<string name="failedOpeningFileManager">Instale un gestor de archivos primero.</string>
<string name="deleteConfirmationGroup">¿Quiere eliminar el grupo\?</string>
<string name="all">Todo</string>
<string name="star">Añadir a favoritos</string>
@@ -86,14 +86,20 @@
<string name="expiryStateSentenceExpired">Expirado: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Expira: <xliff:g>%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Derechos de autor © 2019-<xliff:g>%d</xliff:g> Sylvia van Os y colaboradores</string>
<string name="app_resources">Recursos de terceros: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotecas de terceros: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importCatimaMessage">Seleccione su exportado desde Catima para importarlo.\nCréalo primero desde el menú Importar/Exportar de otra app de Catima al presionar Exportar desde allí.</string>
<string name="importFidmeMessage">Seleccione su exportado desde FidMe para importar, y luego escoja los tipos de códigos de barras manualmente.\nCréalo desde tu perfil de FidMe eligiendo Protección de datos y pulsa Extraer mis datos.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su exportado desde Loyalty Card Keychain para importarlo.\nCréalo desde el menú Importar/Exportar en Loyalty Card Keychain pulsando Exportar desde allí.</string>
<string name="importVoucherVaultMessage">Seleccione su exportado desde Voucher Vault para importarlo.\nCréalo pulsando Exportar en Voucher Vault.</string>
<string name="failedGeneratingShareURL">No se ha podido generar una URL compartible</string>
<string name="passwordRequired">Introduzca la contraseña</string>
<string name="app_resources">Recursos de terceros libres: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotecas de terceros libres: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importCatimaMessage">Seleccione su <i>catima.zip</i> exportado desde Catima para importarlo.
\nCréalo primero desde el menú Importar/Exportar de otra app de Catima al presionar Exportar desde allí.</string>
<string name="importFidmeMessage">Seleccione su <i>fidme-export-request-xxxxxx.zip</i> exportado desde FidMe para importar, y luego escoja los tipos de códigos de barras manualmente.
\nCréalo primero desde tu perfil de FidMe eligiendo Protección de datos y pulsa Extraer mis datos.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su <i>LoyaltyCardKeychain.csv</i> exportado desde Loyalty Card Keychain para importarlo.
\nCréalo primero desde el menú Importar/Exportar en Loyalty Card Keychain pulsando Exportar desde allí.</string>
<string name="importStocardMessage">Seleccione su exportación <i>*.zip</i> de Stocard para importarla.
\nConsígalo enviando un correo electrónico a support@stocardapp.com solicitando una exportación de sus datos.</string>
<string name="importVoucherVaultMessage">Seleccione su <i>vouchervault.json</i> exportado desde Voucher Vault para importarlo.
\nCréalo pulsando primero Exportar en Voucher Vault.</string>
<string name="failedGeneratingShareURL">No se ha podido generar una URL compartible. Por favor, informe de ello.</string>
<string name="passwordRequired">Por favor, introduzca la contraseña</string>
<string name="updateBarcodeQuestionText">Ha cambiado el código. ¿Desea actualizar también el código de barras para usar el mismo valor\?</string>
<string name="intent_import_card_from_url_share_multiple_text">Quiero compartirte algunas tarjetas</string>
<string name="setBackImage">Establecer imagen anversa</string>
@@ -114,6 +120,7 @@
<string name="sameAsCardId">Igual que el código</string>
<string name="barcodeId">Valor de código de barra</string>
<string name="importVoucherVault">Importar desde Voucher Vault</string>
<string name="importStocard">Importar desde Stocard</string>
<string name="importLoyaltyCardKeychain">Importar desde Loyalty Card Keychain</string>
<string name="importFidme">Importar desde FidMe</string>
<string name="importCatima">Importar desde Catima</string>
@@ -159,15 +166,20 @@
<string name="settings_system_locale">Sistema</string>
<string name="settings_locale">Idioma</string>
<string name="noGroupCards">Este grupo está vacío</string>
<string name="settings_landscape_orientation">Horizontal</string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punto</item>
<item quantity="many"><xliff:g>%s</xliff:g> puntos</item>
<item quantity="other"><xliff:g>%s</xliff:g> puntos</item>
</plurals>
<string name="barcodeImageDescriptionWithType">Imagen <xliff:g>%s</xliff:g> código de barras</string>
<string name="settings_card_orientation">Orientación de pantalla</string>
<string name="settings_portrait_orientation">Formato vertical</string>
<string name="group_edit">Editar grupo</string>
<string name="group_updated">Grupo actualizado</string>
<string name="noGiftCardsGroup">Crea algunas tarjetas y luego asígnelas al grupo aquí</string>
<string name="noGiftCardsGroup">Crea algunas tarjetas y luego asígnelas al grupo aquí.</string>
<string name="settings_follow_system_orientation">Segue el sistema</string>
<string name="settings_lock_on_opening_orientation">Bloqueo a la orientación utilizada al abrir la tarjeta</string>
<string name="sort_by_most_recently_used">Lo más usado recientemente</string>
<string name="sort_by_expiry">Caducidad</string>
<string name="version_history">Historial de versiones</string>
@@ -206,7 +218,7 @@
<string name="archived">Tarjeta archivada</string>
<string name="unarchived">Tarjeta desarchivada</string>
<string name="exportPassword">Establezca una contraseña para proteger su exportación (opcional)</string>
<string name="failedLaunchingPhotoPicker">No se encontró una aplicación de galería compatible</string>
<string name="failedLaunchingPhotoPicker">No se ha encontró una aplicación de galería compatible</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> tarjeta (archivada)<xliff:g id="archivedCount">%2$d</xliff:g></item>
<item quantity="many"><xliff:g>%1$d</xliff:g> tarjetas (archivadas)<xliff:g id="archivedCount">%2$d</xliff:g></item>
@@ -266,6 +278,7 @@
<string name="addWithoutBarcode">Añadir una tarjeta sin código de barras</string>
<string name="field_must_not_be_empty">Este campo no debe estar vacío</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Girar siempre (ignora la configuración del sistema)</string>
<string name="continue_">Continuar</string>
<string name="add_manually_warning_title">Se recomienda escanear</string>
<string name="add_manually_warning_message">En algunas tiendas, el valor del código de barras difiere del número escrito en la tarjeta. Por este motivo, es posible que la introducción manual del código de barras no siempre funcione. Se recomienda encarecidamente escanear el código de barras con la cámara. ¿Aún desea continuar?</string>
@@ -295,20 +308,12 @@
<string name="settings_column_count_5">5</string>
<string name="settings_column_count_6">6</string>
<string name="settings_column_count_7">7</string>
<string name="generic_error_please_retry">Algo salió mal</string>
<string name="generic_error_please_retry">Lo sentimos, algo salió mal, por favor inténtelo de nuevo...</string>
<string name="unsupportedFile">Este archivo no es compatible</string>
<string name="addFromPkpass">Seleccione un archivo Passbook (.pkpass / .pkpasses)</string>
<string name="addFromPkpass">Seleccione un archivo Passbook (.pkpass)</string>
<string name="sort_by_valid_from">Válido desde</string>
<string name="setBarcodeWidth">Establecer el ancho del código de barras</string>
<string name="width">Ancho</string>
<string name="card_list_widget_name">Lista de tarjetas</string>
<string name="card_list_widget_empty">Después de añadir algunas tarjetas de fidelidad en Catima, aparecerán aquí. Si tienes tarjetas, asegúrate de que no estén archivadas.</string>
<string name="cardWithNumber">Tarjeta <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Tarjeta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Por favor, no rote el dispositivo, ya que esto cancelará la acción</string>
<string name="acra_catima_has_crashed">Lo sentimos, pero <xliff:g id="app_name">%s</xliff:g> ha fallado. Por favor, ayúdenos a resolver esta incidencia enviándonos un reporte del error.</string>
<string name="acra_explain_crash">Si es posible, por favor añada más detalles sobre lo que estaba haciendo aquí:</string>
<string name="acra_crash_email_subject">Reporte del fallo <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Solicitar envío de reportes de fallos</string>
<string name="pref_enable_acra_summary">Cuando está activado, se le pedirá que informe sobre un fallo cuando ocurra. Los informes de fallo nunca se envían automáticamente.</string>
</resources>

View File

@@ -5,7 +5,7 @@
<item quantity="one"><xliff:g>%d</xliff:g> valitud</item>
<item quantity="other"><xliff:g>%d</xliff:g> valitud</item>
</plurals>
<string name="noGiftCardsGroup">Lisa mõned kaardid ja siis jaga nad gruppidesse</string>
<string name="noGiftCardsGroup">Lisa mõned kaardid ja siis jaga nad gruppidesse.</string>
<string name="noMatchingGiftCards">Tulemusi pole. Palun proovi muuta otsingut.</string>
<string name="storeName">Nimi</string>
<string name="note">Märkus</string>
@@ -32,7 +32,7 @@
<string name="addCardTitle">Lisa kaart</string>
<string name="scanCardBarcode">Skaneeri triipkoodi</string>
<string name="app_name">Catima</string>
<string name="noGiftCards">Kaardi lisamiseks klõpsi + pluss nuppu või impordi ⋮ikooniga menüüst</string>
<string name="noGiftCards">Kaardi lisamiseks klõpsi + pluss nuppu või impordi ⋮ikooniga menüüst.</string>
<string name="action_search">Otsi</string>
<string name="unstar">Eemalda lemmikute hulgast</string>
<string name="cancel">Katkesta</string>
@@ -46,6 +46,9 @@
<string name="settings_dark_theme">Tume kujundus</string>
<string name="thumbnailDescription">Pisipilt</string>
<string name="settings_theme">Kujundus</string>
<string name="settings_card_orientation">Ekraanipaigutuse suund</string>
<string name="settings_follow_sensor_orientation">Alati pööra (eira süsteemset paigutust)</string>
<string name="settings_landscape_orientation">Rõhtvaade</string>
<string name="settings_display_barcode_max_brightness">Tee ekraan eredamaks</string>
<string name="app_license">Copyleft-tüüpi autoriõiguste alusel loodud avatud lähtekoodiga tarkvara, mis on avaldatud GPLv3+ all</string>
<string name="settings_keep_screen_on">Hoia ekraan sisselülitatuna</string>
@@ -53,10 +56,10 @@
<string name="noCardsMessage">Esmalt lisa kaart</string>
<string name="barcodeImageDescriptionWithType">Kaardi <xliff:g>%s</xliff:g> tiipkood</string>
<string name="noCardExistsError">Seda kaarti ei leidu</string>
<string name="failedParsingImportUriError">Importimise aadressi töötlemine ei õnnestunud</string>
<string name="failedParsingImportUriError">Impordi aadressi töötlemine ei õnnestunud</string>
<string name="importExport">Import/eksport</string>
<string name="exportName">Ekspordi</string>
<string name="importExportHelp">Andmete varundamine võimaldab sul neid tõsta mõnda teise seadmesse</string>
<string name="importExportHelp">Andmete varundamine võimaldab sul neid tõsta mõnda teise seadmesse.</string>
<string name="importSuccessfulTitle">Imporditud</string>
<string name="importFailedTitle">Import ei õnnestunud</string>
<string name="exportSuccessfulTitle">Eksporditud</string>
@@ -71,23 +74,26 @@
<string name="permissionReadCardsDescription">loe kõiki oma Catima kaarte koos nende üksikasjadega, sealhulgas märkuste ja piltidega</string>
<string name="cameraPermissionDeniedTitle">Puudub ligipääs kaamerale</string>
<string name="noCameraPermissionDirectToSystemSetting">Triipkoodide skaneerimiseks vajab Catima õigust asutada kaamerat. Õiguste andmiseks klõpsi siin.</string>
<string name="exportOptionExplanation">Andmed salvestame sinu valitud asukohta</string>
<string name="exportOptionExplanation">Andmed salvestame sinu valitud asukohta.</string>
<string name="importOptionFilesystemTitle">Impordi failisüsteemist</string>
<string name="importOptionFilesystemExplanation">Vali vajalik impordifail failisüsteemist</string>
<string name="importOptionFilesystemExplanation">Vali vajalik impordifail failisüsteemist.</string>
<string name="importOptionFilesystemButton">Vali failisüsteemist</string>
<string name="about">Rakenduse teave</string>
<string name="app_copyright_short">Autoriõigused © Sylvia van Os ja kaasautorid</string>
<string name="about_title_fmt">Teave <xliff:g id="app_name">%s</xliff:g> kohta</string>
<string name="debug_version_fmt">Versioon: <xliff:g id="version">%s</xliff:g></string>
<string name="app_libraries">Kolmandate osapoolte teegid: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Kolmandate osapoolte materjalid: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Kolmandate osapoolte avatud lähtekoodiga teegid: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Kolmandate osapoolte avatud lähtekoodiga materjalid: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="starImage">Lemmikut märkiv täht</string>
<string name="settings">Seadistused</string>
<string name="settings_system_theme">Süsteemi kujundus</string>
<string name="settings_follow_system_orientation">Järgi süsteemset paigutust</string>
<string name="settings_portrait_orientation">Püstvaade</string>
<string name="settings_lock_on_opening_orientation">Kaardivaate avamisel lukusta paigutus</string>
<string name="settings_display_barcode_max_brightness_summary">See on vajalik mõnede skännerite toimimiseks</string>
<string name="expiryStateSentenceExpired">Aegus: <xliff:g>%s</xliff:g></string>
<string name="settings_allow_content_provider_read_summary">Selle valiku sisselülitamisel peavad muud rakendused lisaks küsima õigust vaadata kaartide andmeid</string>
<string name="noGroups">Kui soovid sarnaseid kaarte omavahel liigitada, siis + pluss nupuga lisa kaardigruppe</string>
<string name="noGroups">Kui soovid sarnaseid kaarte omavahel liigitada siis + pluss nupuga lisa kaardigruppe.</string>
<string name="group_name_is_empty">Kaardigrupi nimi ei saa jääda tühjaks</string>
<string name="groupsList">Grupid: <xliff:g>%s</xliff:g></string>
<plurals name="groupCardCountWithArchived">
@@ -111,10 +117,10 @@
<string name="expiryDate">Aegumise kuupäev</string>
<string name="never">Mitte kunagi</string>
<string name="showMoreInfo">Näita teavet</string>
<string name="importLoyaltyCardKeychainMessage">Importimiseks vali Loyalty Card Keychaini ekspordifail. \nSellise faili saad teha rakendusest Loyalty Card Keychain valides menüüst Import/Eksport valiku Eksport.</string>
<string name="importLoyaltyCardKeychainMessage">Importimiseks vali oma <i>LoyaltyCardKeychain.csv</i> Loyalty Card Keychaini ekspordifail. \nSellise faili saad teha rakendusest Loyalty Card Keychain valides menüüst Import/Eksport valiku Eksport.</string>
<string name="unsupportedBarcodeType">Sellist triipkoodi tüüpi pole veel võimalik kuvada, aga mõnes hilisemas rakenduse versioonis võib see võimalik olla.</string>
<string name="wrongValueForBarcodeType">Väärtus ei sobi selle triipkoodi tüübiga</string>
<string name="passwordRequired">Sisesta salasõna</string>
<string name="passwordRequired">Palun sisesta salasõna</string>
<string name="updateBarcodeQuestionTitle">Kas uuendame triipkoodi väärtust?</string>
<string name="yes">Jah</string>
<string name="no">Ei</string>
@@ -157,9 +163,9 @@
<string name="group_updated">Kaardigrupp on uuendatud</string>
<string name="deleteConfirmationGroup">Kas kustutame grupi?</string>
<string name="all">Kõik</string>
<string name="failedOpeningFileManager">Failihalduri avamine ei õnnestunud</string>
<string name="failedOpeningFileManager">Esmalt paigalda failihaldur.</string>
<string name="intent_import_card_from_url_share_text">Ma soovin sinuga jagada ühte oma kliendikaarti</string>
<string name="editGroup">Grupp on muutmisel: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Muudame gruppi: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Aegub: <xliff:g>%s</xliff:g></string>
<string name="moveBarcodeToTopOfScreen">Tõsta triipkood ekraani ülaossa</string>
<string name="noBarcodeFound">Ühtegi triipkoodi ei leidunud</string>
@@ -172,13 +178,15 @@
<string name="privacy_policy">Andmekaitsepõhimõtted</string>
<string name="accept">Nõustu</string>
<string name="importCatima">Impordi Catima varukoopiast</string>
<string name="importCatimaMessage">Importimiseks vali varem tehtud Catima ekspordifail. \nSellise faili saad luua mõnes teises seadmes olevast Catima rakendusest Import/Eksport menüüst valikust Eksport.</string>
<string name="importCatimaMessage">Importimiseks vali varem tehtud <i>catima.zip</i> Catima ekspordifail. \nSellise faili saad luua mõnes teises seadmes olevast Catima rakendusest Import/Eksport menüüst valikust Eksport.</string>
<string name="importFidme">Impordi FidMe varukoopiast</string>
<string name="importFidmeMessage">Importimiseks vali fail, mille oled FidMe rakendusest eksportinud. Peale importi määra triipkoodi tüübid käsistsi. \nSellise faili loomiseks vali oma FidMe profiilist eelistuse Andmekaitse-Paki lahti.</string>
<string name="importFidmeMessage">Importimiseks vali oma <i>fidme-export-request-xxxxxx.zip</i>, mille oled FidMe rakendusest eksportinud. Peale importi määra triipkoodi tüübid käsistsi. \nSellise faili loomiseks vali oma FidMe profiilist eelistuse Andmekaitse-Paki lahti.</string>
<string name="importLoyaltyCardKeychain">Impordi rakendusest Loyalty Card Keychain</string>
<string name="importStocard">Impordi Stocardist</string>
<string name="importStocardMessage">Importimiseks vali oma <i>***.zip</i> Stocardi ekspordifail. \nSellise faili saad saates kirja aadressile support@stocardapp.com ning küsides oma andmeid.</string>
<string name="chooseImportType">Importimise valikud</string>
<string name="importVoucherVault">Impordi rakendusest Voucher Vault</string>
<string name="importVoucherVaultMessage">Importimiseks vali oma Voucher Vaulti ekspordifail. \nSellise faili saad teha rakenduses Voucher Vault menüüvalikust Eksport.</string>
<string name="importVoucherVaultMessage">Importimiseks vali oma <i>vouchervault.json</i> Voucher Vaulti ekspordifail. \nSellise faili saad teha rakenduses Voucher Vault menüüvalikust Eksport.</string>
<string name="barcodeId">Triipkoodi väärtus</string>
<string name="sameAsCardId">Sama, kui ID</string>
<string name="setBarcodeId">Sisesta triipkoodi väärtus</string>
@@ -192,7 +200,7 @@
<string name="updateBarcodeQuestionText">Sa muutsid ID väärtust? Kas sa soovid ka triipkoodiväärtuse vastavalt uuendada?</string>
<string name="exportPassword">Sinu eksporditavate andmete turvamiseks palun sisesta salasõna (kui soovid seda)</string>
<string name="exportPasswordHint">Sisesta salasõna</string>
<string name="failedGeneratingShareURL">Jagatava võrguaadressi loomine ei õnnestunud</string>
<string name="failedGeneratingShareURL">Jagatava võrguaadressi loomine ei õnnestunud. Palun anna sellest meile teada.</string>
<string name="turn_flashlight_on">Lülita taskulamp sisse</string>
<string name="turn_flashlight_off">Lülita taskulamp välja</string>
<string name="settings_locale">Keel</string>
@@ -226,7 +234,7 @@
<string name="unarchive">Eemalda arhiivist</string>
<string name="archived">Kaart on arhiveeritud</string>
<string name="unarchived">Kaart on arhiivist eemaldatud</string>
<string name="failedLaunchingPhotoPicker">Ei õnnestunud leida toetatud pildivalijat</string>
<string name="failedLaunchingPhotoPicker">Ei õnnestunud leida toetatud galeriirakendust</string>
<string name="previousCard">Eelmine</string>
<string name="nextCard">Järgmine</string>
<string name="failedToOpenUrl">Esmalt paigalda veebibrauser</string>
@@ -239,8 +247,8 @@
<string name="switchToFrontImage">Vaata esikülje pilti</string>
<string name="switchToBackImage">Vaata tagakülje pilti</string>
<string name="switchToBarcode">Vaata triipkoodi</string>
<string name="openFrontImageInGalleryApp">Ava esikülje pilt pildivalijas</string>
<string name="openBackImageInGalleryApp">Ava tagakülje pilt pildivalijas</string>
<string name="openFrontImageInGalleryApp">Ava esikülje pilt galeriirakenduses</string>
<string name="openBackImageInGalleryApp">Ava tagakülje pilt galeriirakenduses</string>
<string name="setBarcodeHeight">Määra triipkoodi kõrgus</string>
<string name="donate">Toeta rahaliselt</string>
<string name="icon_header_click_text">Pisipildi muutmiseks vajuta pikalt</string>
@@ -263,7 +271,7 @@
<string name="field_must_not_be_empty">Väli ei tohi olla tühi</string>
<string name="manually_enter_barcode_instructions">Sisesta sinu kaardil kuvatav tunnusnumber või -tekst ja klõpsi triipkoodi, millelaadset kuvatakse kaardil.</string>
<string name="add_manually_warning_title">Soovitame, et skaneerid triipkoodi</string>
<string name="add_manually_warning_message">Mõnede poodide ja äride puhul triipkoodi väärtus erineb kaardile kirjutatud numbrist. Seetõttu ei pruugi triipkoodi käsitsi lisamine alati toimida. Me soovitame, et pigem skaneerid triipkoodi kaameraga. Kas sa siiski soovid jätkata?</string>
<string name="add_manually_warning_message">Mõnede poodide ja äride puhul triipkoodi väärtus erineb kaardile kirjutatud numbrist. Seetõttu ei pruugi triipkoodi käsitsi lisamine alati toimida. Me tungivalt soovitame, et pigem skaneerid triipkoodi kaameraga. Kas sa siiski soovid jätkata?</string>
<string name="continue_">Jätka</string>
<string name="spend">Kuluta</string>
<string name="receive">Võta vastu</string>
@@ -289,20 +297,12 @@
<string name="settings_column_count_6">6</string>
<string name="settings_column_count_4">4</string>
<string name="settings_column_count_7">7</string>
<string name="generic_error_please_retry">Tekkis viga</string>
<string name="generic_error_please_retry">Vabandust, midagi läks nüüd viltu, palun proovi uuesti...</string>
<string name="unsupportedFile">See fail pole toetatud</string>
<string name="addFromPkpass">Vali Passbooki fail (.pkpass / .pkpasses)</string>
<string name="addFromPkpass">Vali Passbooki fail (.pkpass)</string>
<string name="sort_by_valid_from">Kehtib alates</string>
<string name="setBarcodeWidth">Määratle triipkoodi laius</string>
<string name="width">Laius</string>
<string name="card_list_widget_name">Kaartide loend</string>
<string name="card_list_widget_empty">Kui lisad Catimasse kliendikaarte, siis saavad nad olema nähtavad siin. Kui sul on kaardid lisatud, siis palun kontrolli, et nad kõik poleks arhiveeritud.</string>
<string name="cardWithNumber">Kaart: <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Kaart: <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Palun ära pööra nutiseadet - see katkestab tegevuse</string>
<string name="acra_catima_has_crashed">Vabandus, aga <xliff:g id="app_name">%s</xliff:g> on kokku jooksnud. Kui saadad meile veakirjelduse, siis aitad seda viga parandada.</string>
<string name="acra_explain_crash">Kui vähegi võimalik, siis palun kirjelda, mida sa antud hetkel tegid:</string>
<string name="acra_crash_email_subject">Kokkujooksmise aruanne: <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Küsi luba kokkujooksmiste aruannete saatmiseks</string>
<string name="pref_enable_acra_summary">Kui eelistus on kasutusel, siis rakendus küsib sinult luba veateate saatmiseks. Seda ei tehta iialgi automaatselt.</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More