From 667ce42731c95a98926657fea359b56209f9348e Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Sun, 4 Oct 2020 21:37:23 +0300 Subject: [PATCH] UI and machinery for detecting hledger-web version todo: retry discovering version upon URL change, make JSON input/output honour the version --- .../mobileledger/model/HledgerVersion.java | 90 +++++++++++++++++++ .../model/MobileLedgerProfile.java | 40 ++++++++- .../ui/profiles/ProfileDetailFragment.java | 75 ++++++++++------ .../ui/profiles/ProfileDetailModel.java | 84 ++++++++++++++++- .../utils/MobileLedgerDatabase.java | 2 +- .../ktnx/mobileledger/utils/NetworkUtil.java | 27 ++++-- .../ic_refresh_primary_24dp.xml | 31 +++++++ app/src/main/res/layout/profile_detail.xml | 51 +++++++++-- app/src/main/res/raw/create_db.sql | 2 +- app/src/main/res/raw/sql_41.sql | 17 ++++ app/src/main/res/values-bg/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 12 files changed, 375 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java create mode 100644 app/src/main/res/drawable-anydpi/ic_refresh_primary_24dp.xml create mode 100644 app/src/main/res/raw/sql_41.sql diff --git a/app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java b/app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java new file mode 100644 index 00000000..f3cbba21 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java @@ -0,0 +1,90 @@ +/* + * Copyright © 2020 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Locale; + +public class HledgerVersion { + private int major; + private int minor; + private int patch; + private boolean isPre_1_20; + private boolean hasPatch; + public HledgerVersion(int major, int minor) { + this.major = major; + this.minor = minor; + this.isPre_1_20 = false; + this.hasPatch = false; + } + public HledgerVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.isPre_1_20 = false; + this.hasPatch = true; + } + public HledgerVersion(boolean pre_1_20) { + if (!pre_1_20) + throw new IllegalArgumentException("pre_1_20 argument must be true"); + this.major = this.minor = 0; + this.isPre_1_20 = true; + this.hasPatch = false; + } + public HledgerVersion(HledgerVersion origin) { + this.major = origin.major; + this.minor = origin.minor; + this.isPre_1_20 = origin.isPre_1_20; + this.patch = origin.patch; + this.hasPatch = origin.hasPatch; + } + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) + return false; + if (!(obj instanceof HledgerVersion)) + return false; + HledgerVersion that = (HledgerVersion) obj; + + return (this.isPre_1_20 == that.isPre_1_20 && this.major == that.major && + this.minor == that.minor && this.patch == that.patch && + this.hasPatch == that.hasPatch); + } + public boolean isPre_1_20() { + return isPre_1_20; + } + public int getMajor() { + return major; + } + public int getMinor() { + return minor; + } + public int getPatch() { + return patch; + } + @NonNull + @Override + public String toString() { + if (isPre_1_20) + return "(before 1.20)"; + return hasPatch ? String.format(Locale.ROOT, "%d.%d.%d", major, minor, patch) + : String.format(Locale.ROOT, "%d.%d", major, minor); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java index c9e358e2..55746ddd 100644 --- a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java +++ b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java @@ -63,6 +63,7 @@ public final class MobileLedgerProfile { private FutureDates futureDates = FutureDates.None; private boolean accountsLoaded; private boolean transactionsLoaded; + private HledgerVersion detectedVersion; // N.B. when adding new fields, update the copy-constructor below transient private AccountAndTransactionListSaver accountAndTransactionListSaver; public MobileLedgerProfile(String uuid) { @@ -86,6 +87,8 @@ public final class MobileLedgerProfile { defaultCommodity = origin.defaultCommodity; accountsLoaded = origin.accountsLoaded; transactionsLoaded = origin.transactionsLoaded; + if (origin.detectedVersion != null) + detectedVersion = new HledgerVersion(origin.detectedVersion); } // loads all profiles into Data.profiles // returns the profile with the given UUID @@ -97,7 +100,8 @@ public final class MobileLedgerProfile { "auth_password, permit_posting, theme, order_no, " + "preferred_accounts_filter, future_dates, api_version, " + "show_commodity_by_default, default_commodity, " + - "show_comments_by_default FROM " + + "show_comments_by_default, detected_version_pre_1_19, " + + "detected_version_major, detected_version_minor FROM " + "profiles order by order_no", null)) { while (cursor.moveToNext()) { @@ -116,6 +120,21 @@ public final class MobileLedgerProfile { item.setShowCommodityByDefault(cursor.getInt(12) == 1); item.setDefaultCommodity(cursor.getString(13)); item.setShowCommentsByDefault(cursor.getInt(14) == 1); + { + boolean pre_1_20 = cursor.getInt(15) == 1; + int major = cursor.getInt(16); + int minor = cursor.getInt(17); + + if (!pre_1_20 && major == 0 && minor == 0) { + item.detectedVersion = null; + } + else if (pre_1_20) { + item.detectedVersion = new HledgerVersion(true); + } + else { + item.detectedVersion = new HledgerVersion(major, minor); + } + } list.add(item); if (item.getUuid() .equals(currentProfileUUID)) @@ -142,6 +161,12 @@ public final class MobileLedgerProfile { db.endTransaction(); } } + public HledgerVersion getDetectedVersion() { + return detectedVersion; + } + public void setDetectedVersion(HledgerVersion detectedVersion) { + this.detectedVersion = detectedVersion; + } @Contract(value = "null -> false", pure = true) @Override public boolean equals(@Nullable Object obj) { @@ -179,6 +204,8 @@ public final class MobileLedgerProfile { return false; if (apiVersion != p.apiVersion) return false; + if (!Objects.equals(detectedVersion, p.detectedVersion)) + return false; return futureDates == p.futureDates; } public boolean getShowCommentsByDefault() { @@ -294,13 +321,18 @@ public final class MobileLedgerProfile { db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " + "use_authentication, auth_user, auth_password, theme, order_no, " + "preferred_accounts_filter, future_dates, api_version, " + - "show_commodity_by_default, default_commodity, show_comments_by_default) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "show_commodity_by_default, default_commodity, show_comments_by_default," + + "detected_version_pre_1_19, detected_version_major, " + + "detected_version_minor) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{uuid, name, permitPosting, url, authEnabled, authEnabled ? authUserName : null, authEnabled ? authPassword : null, themeHue, orderNo, preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(), - showCommodityByDefault, defaultCommodity, showCommentsByDefault + showCommodityByDefault, defaultCommodity, showCommentsByDefault, + (detectedVersion != null) && detectedVersion.isPre_1_20(), + (detectedVersion == null) ? 0 : detectedVersion.getMajor(), + (detectedVersion == null) ? 0 : detectedVersion.getMinor() }); db.setTransactionSuccessful(); } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java index 72c4ed4c..9b0b60b7 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java @@ -282,33 +282,26 @@ public class ProfileDetailFragment extends Fragment { apiVersionText = context.findViewById(R.id.api_version_text); model.observeApiVersion(viewLifecycleOwner, apiVer -> apiVersionText.setText(apiVer.getDescription(getResources()))); - context.findViewById(R.id.api_version_layout) - .setOnClickListener(v -> { - MenuInflater mi = new MenuInflater(context); - PopupMenu menu = new PopupMenu(context, v); - menu.inflate(R.menu.api_version); - menu.setOnMenuItemClickListener(item -> { - SendTransactionTask.API apiVer; - switch (item.getItemId()) { - case R.id.api_version_menu_html: - apiVer = SendTransactionTask.API.html; - break; - case R.id.api_version_menu_post_1_14: - apiVer = SendTransactionTask.API.post_1_14; - break; - case R.id.api_version_menu_pre_1_15: - apiVer = SendTransactionTask.API.pre_1_15; - break; - case R.id.api_version_menu_auto: - default: - apiVer = SendTransactionTask.API.auto; - } - model.setApiVersion(apiVer); - apiVersionText.setText(apiVer.getDescription(getResources())); - return true; - }); - menu.show(); - }); + context.findViewById(R.id.api_version_label) + .setOnClickListener(this::chooseAPIVersion); + context.findViewById(R.id.api_version_text) + .setOnClickListener(this::chooseAPIVersion); + + TextView detectedApiVersion = context.findViewById(R.id.detected_version_text); + model.observeDetectedVersion(viewLifecycleOwner, ver -> { + if (ver == null) + detectedApiVersion.setText(context.getResources() + .getString(R.string.api_version_unknown_label)); + else if (ver.isPre_1_20()) + detectedApiVersion.setText(context.getResources() + .getString(R.string.api_pre_1_19)); + else + detectedApiVersion.setText(ver.toString()); + }); + detectedApiVersion.setOnClickListener(v -> model.triggerVersionDetection()); + context.findViewById(R.id.api_version_detect_button) + .setOnClickListener(v -> model.triggerVersionDetection()); + authParams = context.findViewById(R.id.auth_params); useAuthentication = context.findViewById(R.id.enable_http_auth); @@ -386,6 +379,34 @@ public class ProfileDetailFragment extends Fragment { profileName.requestFocus(); } + private void chooseAPIVersion(View v) { + Activity context = getActivity(); + ProfileDetailModel model = getModel(); + MenuInflater mi = new MenuInflater(context); + PopupMenu menu = new PopupMenu(context, v); + menu.inflate(R.menu.api_version); + menu.setOnMenuItemClickListener(item -> { + SendTransactionTask.API apiVer; + switch (item.getItemId()) { + case R.id.api_version_menu_html: + apiVer = SendTransactionTask.API.html; + break; + case R.id.api_version_menu_post_1_14: + apiVer = SendTransactionTask.API.post_1_14; + break; + case R.id.api_version_menu_pre_1_15: + apiVer = SendTransactionTask.API.pre_1_15; + break; + case R.id.api_version_menu_auto: + default: + apiVer = SendTransactionTask.API.auto; + } + model.setApiVersion(apiVer); + apiVersionText.setText(apiVer.getDescription(getResources())); + return true; + }); + menu.show(); + } private MobileLedgerProfile.FutureDates futureDatesSettingFromMenuItemId(int itemId) { switch (itemId) { case R.id.menu_future_dates_7: diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java index 915ab032..92d1505f 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java @@ -26,9 +26,22 @@ import androidx.lifecycle.ViewModel; import net.ktnx.mobileledger.async.SendTransactionTask; import net.ktnx.mobileledger.model.Currency; +import net.ktnx.mobileledger.model.HledgerVersion; import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.utils.Colors; +import net.ktnx.mobileledger.utils.Logger; import net.ktnx.mobileledger.utils.Misc; +import net.ktnx.mobileledger.utils.NetworkUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ProfileDetailModel extends ViewModel { private static final String HTTPS_URL_START = "https://"; @@ -47,7 +60,9 @@ public class ProfileDetailModel extends ViewModel { private final MutableLiveData authPassword = new MutableLiveData<>(null); private final MutableLiveData preferredAccountsFilter = new MutableLiveData<>(null); private final MutableLiveData themeId = new MutableLiveData<>(-1); + private final MutableLiveData detectedVersion = new MutableLiveData<>(null); public int initialThemeHue = Colors.DEFAULT_HUE_DEG; + private VersionDetectionThread versionDetectionThread; public ProfileDetailModel() { } String getProfileName() { @@ -131,6 +146,14 @@ public class ProfileDetailModel extends ViewModel { void observeApiVersion(LifecycleOwner lfo, Observer o) { apiVersion.observe(lfo, o); } + HledgerVersion getDetectedVersion() { return detectedVersion.getValue(); } + void setDetectedVersion(HledgerVersion newValue) { + if (!Objects.equals(detectedVersion.getValue(), newValue)) + detectedVersion.setValue(newValue); + } + void observeDetectedVersion(LifecycleOwner lfo, Observer o) { + detectedVersion.observe(lfo, o); + } String getUrl() { return url.getValue(); } @@ -218,6 +241,7 @@ public class ProfileDetailModel extends ViewModel { authPassword.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : ""); preferredAccountsFilter.setValue(mProfile.getPreferredAccountsFilter()); themeId.setValue(mProfile.getThemeHue()); + detectedVersion.setValue(mProfile.getDetectedVersion()); } else { profileName.setValue(null); @@ -232,9 +256,8 @@ public class ProfileDetailModel extends ViewModel { authPassword.setValue(""); preferredAccountsFilter.setValue(null); themeId.setValue(newProfileHue); + detectedVersion.setValue(null); } - - } void updateProfile(MobileLedgerProfile mProfile) { mProfile.setName(profileName.getValue()); @@ -251,5 +274,62 @@ public class ProfileDetailModel extends ViewModel { mProfile.setThemeHue(themeId.getValue()); mProfile.setFutureDates(futureDates.getValue()); mProfile.setApiVersion(apiVersion.getValue()); + mProfile.setDetectedVersion(detectedVersion.getValue()); + } + synchronized public void triggerVersionDetection() { + if (versionDetectionThread != null) + versionDetectionThread.interrupt(); + + versionDetectionThread = new VersionDetectionThread(this); + versionDetectionThread.start(); + } + static class VersionDetectionThread extends Thread { + private final Pattern versionPattern = + Pattern.compile("^\"(\\d+)\\.(\\d+)(?:\\.(\\d+))?\"$"); + private final ProfileDetailModel model; + public VersionDetectionThread(ProfileDetailModel model) { + this.model = model; + } + @Override + public void run() { + try { + HttpURLConnection http = NetworkUtil.prepareConnection(model.getUrl(), "version", + model.getUseAuthentication()); + switch (http.getResponseCode()) { + case 200: + break; + case 404: + model.detectedVersion.postValue(new HledgerVersion(true)); + return; + default: + Logger.debug("profile", String.format(Locale.US, + "HTTP error detecting hledger-web version: [%d] %s", + http.getResponseCode(), http.getResponseMessage())); + model.detectedVersion.postValue(null); + return; + } + InputStream stream = http.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String version = reader.readLine(); + Matcher m = versionPattern.matcher(version); + if (m.matches()) { + int major = Integer.parseInt(Objects.requireNonNull(m.group(1))); + int minor = Integer.parseInt(Objects.requireNonNull(m.group(2))); + final boolean hasPatch = m.groupCount() >= 3; + int patch = hasPatch ? Integer.parseInt(Objects.requireNonNull(m.group(3))) : 0; + + model.detectedVersion.postValue( + hasPatch ? new HledgerVersion(major, minor, patch) + : new HledgerVersion(major, minor)); + } + else { + Logger.debug("profile", + String.format("Unrecognised version string '%s'", version)); + } + } + catch (IOException e) { + e.printStackTrace(); + } + } } } diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java b/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java index 29f41425..8f8be513 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java @@ -39,7 +39,7 @@ import static net.ktnx.mobileledger.utils.Logger.debug; public class MobileLedgerDatabase extends SQLiteOpenHelper { public static final MutableLiveData initComplete = new MutableLiveData<>(false); private static final String DB_NAME = "MoLe.db"; - private static final int LATEST_REVISION = 40; + private static final int LATEST_REVISION = 41; private static final String CREATE_DB_SQL = "create_db"; private final Application mContext; diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java index c0802a45..c9f0151b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java +++ b/app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019 Damyan Ivanov. + * Copyright © 2020 Damyan Ivanov. * This file is part of MoLe. * MoLe is free software: you can distribute it and/or modify it * under the term of the GNU General Public License as published by @@ -17,8 +17,12 @@ package net.ktnx.mobileledger.utils; +import androidx.annotation.NonNull; + import net.ktnx.mobileledger.model.MobileLedgerProfile; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; @@ -27,14 +31,19 @@ import static net.ktnx.mobileledger.utils.Logger.debug; public final class NetworkUtil { private static final int thirtySeconds = 30000; - public static HttpURLConnection prepareConnection(MobileLedgerProfile profile, String path) - throws IOException { - String url = profile.getUrl(); - final boolean use_auth = profile.isAuthEnabled(); - if (!url.endsWith("/")) url += "/"; - url += path; - debug("network", "Connecting to " + url); - HttpURLConnection http = (HttpURLConnection) new URL(url).openConnection(); + @NotNull + public static HttpURLConnection prepareConnection(@NonNull MobileLedgerProfile profile, + @NonNull String path) throws IOException { + return prepareConnection(profile.getUrl(), path, profile.isAuthEnabled()); + } + public static HttpURLConnection prepareConnection(@NonNull String url, @NonNull String path, + boolean authEnabled) throws IOException { + String connectURL = url; + if (!connectURL.endsWith("/")) + connectURL += "/"; + connectURL += path; + debug("network", "Connecting to " + connectURL); + HttpURLConnection http = (HttpURLConnection) new URL(connectURL).openConnection(); http.setAllowUserInteraction(true); http.setRequestProperty("Accept-Charset", "UTF-8"); http.setInstanceFollowRedirects(false); diff --git a/app/src/main/res/drawable-anydpi/ic_refresh_primary_24dp.xml b/app/src/main/res/drawable-anydpi/ic_refresh_primary_24dp.xml new file mode 100644 index 00000000..4ddd8359 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_refresh_primary_24dp.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/layout/profile_detail.xml b/app/src/main/res/layout/profile_detail.xml index 9442604e..d2ca5601 100644 --- a/app/src/main/res/layout/profile_detail.xml +++ b/app/src/main/res/layout/profile_detail.xml @@ -129,26 +129,65 @@ - + android:textAppearance="?android:textAppearanceListItem" + app:layout_constraintTop_toTopOf="parent" + /> - + android:textColor="?attr/textColor" + app:layout_constraintEnd_toStartOf="@id/detected_version_text" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/api_version_label" + /> + + + + . +create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1, detected_version_pre_1_19 boolean, detected_version_major integer, detected_version_minor integer); create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, level integer not null, parent_name varchar, expanded default 1, amounts_expanded boolean default 0, generation integer default 0); create unique index un_accounts on accounts(profile, name); create table options(profile varchar not null, name varchar not null, value varchar); @@ -20,7 +21,6 @@ create table account_values(profile varchar not null, account varchar not null, create unique index un_account_values on account_values(profile,account,currency); create table description_history(description varchar not null primary key, description_upper varchar, generation integer default 0); create unique index un_description_history on description_history(description_upper); -create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1); create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, generation integer default 0); create unique index un_transactions_id on transactions(profile,id); create unique index un_transactions_data_hash on transactions(profile,data_hash); diff --git a/app/src/main/res/raw/sql_41.sql b/app/src/main/res/raw/sql_41.sql new file mode 100644 index 00000000..5026276a --- /dev/null +++ b/app/src/main/res/raw/sql_41.sql @@ -0,0 +1,17 @@ +-- Copyright © 2020 Damyan Ivanov. +-- This file is part of MoLe. +-- MoLe is free software: you can distribute it and/or modify it +-- under the term of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your opinion), any later version. +-- +-- MoLe is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License terms for details. +-- +-- You should have received a copy of the GNU General Public License +-- along with MoLe. If not, see . +alter table profiles add detected_version_pre_1_19 boolean; +alter table profiles add detected_version_major integer; +alter table profiles add detected_version_minor integer; \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 01ca5802..71308242 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -156,4 +156,7 @@ Заглавна част на страничния панел %,d движения към %s %,d сметки към %s + Неизвестна + Преди 1.20.? + Открита версия diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2778846e..bd3cb031 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -166,4 +166,7 @@ Sub-accounts expand/collapse trigger %,d transactions as of %s %,d accounts as of %s + Unknown + Before 1.20.? + Detected version -- 2.39.2