From 29b2ac2cb65bbd477c8fa4466bbb8772a2ccfe62 Mon Sep 17 00:00:00 2001 From: sspanak Date: Thu, 29 Feb 2024 16:45:20 +0200 Subject: [PATCH] * dictionary exporting * removed unnecessary passing of DictionaryLoader and DictionaryLoadingBar between the preference fragments --- README.md | 1 + app/src/main/AndroidManifest.xml | 6 +- .../tt9/db/exporter/AbstractExporter.java | 149 +++++++++++++++++ .../tt9/db/exporter/CustomWordsExporter.java | 59 +++++++ .../tt9/db/exporter/DictionaryExporter.java | 86 ++++++++++ .../github/sspanak/tt9/db/sqlite/ReadOps.java | 28 +++- .../tt9/preferences/PreferencesActivity.java | 13 -- .../tt9/preferences/items/ItemClickable.java | 57 ++++--- .../preferences/items/ItemExportAbstract.java | 93 +++++++++++ .../items/ItemExportCustomWords.java | 42 +++++ .../items/ItemExportDictionary.java | 62 +++++++ .../preferences/items/ItemLoadDictionary.java | 20 ++- .../preferences/items/ItemTruncateAll.java | 17 +- .../items/ItemTruncateUnselected.java | 10 +- .../screens/DictionariesScreen.java | 83 ++++++++-- .../sspanak/tt9/ui/DictionaryLoadingBar.java | 105 +----------- .../tt9/ui/DictionaryNotification.java | 154 ++++++++++++++++++ app/src/main/res/values-bg/strings.xml | 9 + app/src/main/res/values-de/strings.xml | 9 + app/src/main/res/values-es/strings.xml | 9 + app/src/main/res/values-fr/strings.xml | 9 + app/src/main/res/values-it/strings.xml | 9 + app/src/main/res/values-iw/strings.xml | 9 + app/src/main/res/values-nl/strings.xml | 9 + app/src/main/res/values-pt-rBR/strings.xml | 9 + app/src/main/res/values-ru/strings.xml | 9 + app/src/main/res/values-uk/strings.xml | 9 + app/src/main/res/values/strings.xml | 11 ++ .../res/xml/prefs_screen_dictionaries.xml | 18 ++ docs/user-manual.md | 52 +++--- 30 files changed, 953 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/db/exporter/AbstractExporter.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportAbstract.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportCustomWords.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportDictionary.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/ui/DictionaryNotification.java diff --git a/README.md b/README.md index d92e207b..a03bec1e 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ So make sure to read the initial setup and the hotkey tips in the [user manual]( ## ⌨ Contributing As with many other open-source projects, this one is also maintained by its author in his free time. Any help in making Traditional T9 better will be highly appreciated. Here is how: - Add [a new language](CONTRIBUTING.md#adding-a-new-language), [new UI translations](CONTRIBUTING.md#translating-the-ui) or simply fix a spelling mistake. The process is very simple and even with minimum technical knowledge, your skills as a native speaker will be of great use. Or, if you are not tech-savvy, just [open a new issue](https://github.com/sspanak/tt9/issues) and put the correct translations or words there. Correcting misspelled words or adding new ones is the best you can do to help. Processing millions of words in multiple languages is a very difficult task for a single person. +- Share your list of added words. Use the Export function in: Settings → Languages → Added Words and upload the generated CSV file in a [new issue](https://github.com/sspanak/tt9/issues). You are also welcome to [open a PR](https://github.com/sspanak/tt9/pulls), if you have good technical knowledge and can split them by language. - [Report bugs](https://github.com/sspanak/tt9/issues) or other unusual behavior on different phones. Currently, the only testing and development devices are: Qin F21 Pro+ / Android 11; Energizer H620SEU / Android 10; Vodaphone VFD 500 / Android 6.0. But Android behaviour and appearance varies a lot across the millions of devices available out there. - Experienced developers who are willing fix a bug, or maybe create a brand new feature, see the [Contribution Guide](CONTRIBUTING.md). diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eccb2d5d..af728bbb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ + + successHandler; + protected Thread processThread; + private String outputFile; + + + public static AbstractExporter getInstance() { + throw new RuntimeException("Not Implemented"); + } + + + private void writeAndroid10(Activity activity) throws Exception { + final String fileName = generateFileName(); + outputFile = getOutputDir() + File.pathSeparator + fileName; + + final ContentValues file = new ContentValues(); + file.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); + file.put(MediaStore.MediaColumns.MIME_TYPE, MIME_TYPE); + file.put(MediaStore.MediaColumns.RELATIVE_PATH, getOutputDir()); + + final ContentResolver resolver = activity.getContentResolver(); + Uri uri = null; + + try { + uri = resolver.insert(MediaStore.Files.getContentUri("external"), file); + if (uri == null) { + throw new IOException("Failed to create new MediaStore entry."); + } + + try (OutputStream stream = resolver.openOutputStream(uri)) { + if (stream == null) { + throw new IOException("Failed to open output stream."); + } + stream.write(getWords(activity)); + } + } catch (IOException e) { + if (uri != null) { + resolver.delete(uri, null, null); + } + + throw e; + } + } + + + protected void writeLegacy(Activity activity) throws Exception { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + ) { + activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0); + } + + final String exportDir = Environment.getExternalStoragePublicDirectory(getOutputDir()).getAbsolutePath(); + final String fileName = generateFileName(); + outputFile = getOutputDir() + File.pathSeparator + fileName; + + final File file = new File(exportDir, fileName); + if (!file.createNewFile()) { + throw new IOException("Failed to create a new file."); + } + + try (OutputStream stream = new FileOutputStream(file)) { + stream.write(getWords(activity)); + } + + MediaScannerConnection.scanFile(activity, new String[]{file.getAbsolutePath()}, new String[]{MIME_TYPE}, null); + } + + + protected void write(Activity activity) throws Exception { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + writeAndroid10(activity); + } else { + writeLegacy(activity); + } + } + + + protected String getOutputFile() { + return outputFile; + } + + + public String getOutputDir() { + // on some older phones, files may not be visible in the DOCUMENTS directory, so we use DOWNLOADS + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? Environment.DIRECTORY_DOCUMENTS : Environment.DIRECTORY_DOWNLOADS; + } + + + protected void sendSuccess() { + if (successHandler != null) { + successHandler.accept(outputFile); + } + } + + + protected void sendFailure() { + if (failureHandler != null) { + failureHandler.run(); + } + } + + + public boolean isRunning() { + return processThread != null && processThread.isAlive(); + } + + + public void setSuccessHandler(ConsumerCompat handler) { + successHandler = handler; + } + + + public void setFailureHandler(Runnable handler) { + failureHandler = handler; + } + + + @NonNull abstract protected String generateFileName(); + @NonNull abstract protected byte[] getWords(Activity activity) throws Exception; + abstract public boolean export(Activity activity); +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java b/app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java new file mode 100644 index 00000000..e84c9952 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java @@ -0,0 +1,59 @@ +package io.github.sspanak.tt9.db.exporter; + +import android.app.Activity; +import android.database.sqlite.SQLiteDatabase; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.db.sqlite.ReadOps; +import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; + +public class CustomWordsExporter extends AbstractExporter { + private static CustomWordsExporter customWordsExporterSelf; + + public static final String LOG_TAG = "dictionary_export"; + private static final String BASE_FILE_NAME = "tt9-added-words-export-"; + + public static CustomWordsExporter getInstance() { + if (customWordsExporterSelf == null) { + customWordsExporterSelf = new CustomWordsExporter(); + } + + return customWordsExporterSelf; + } + + public boolean export(Activity activity) { + if (isRunning()) { + return false; + } + + processThread = new Thread(() -> { + try { + write(activity); + sendSuccess(); + } catch (Exception e) { + sendFailure(); + } + }); + + processThread.start(); + return true; + } + + @Override + @NonNull + protected String generateFileName() { + return BASE_FILE_NAME + "-" + System.currentTimeMillis() + FILE_EXTENSION; + } + + @NonNull + @Override + protected byte[] getWords(Activity activity) throws Exception { + SQLiteDatabase db = SQLiteOpener.getInstance(activity).getDb(); + if (db == null) { + throw new Exception("Could not open database"); + } + + return new ReadOps().getWords(db, null, true).getBytes(); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java b/app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java new file mode 100644 index 00000000..ee9f0406 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java @@ -0,0 +1,86 @@ +package io.github.sspanak.tt9.db.exporter; + +import android.app.Activity; +import android.database.sqlite.SQLiteDatabase; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.db.sqlite.ReadOps; +import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; +import io.github.sspanak.tt9.languages.Language; + +public class DictionaryExporter extends AbstractExporter { + private static DictionaryExporter self; + + public static final String LOG_TAG = "dictionary_export"; + private static final String BASE_FILE_NAME = "tt9-dictionary-export-"; + private ArrayList languages; + private Language currentLanguage; + + public static DictionaryExporter getInstance() { + if (self == null) { + self = new DictionaryExporter(); + } + + return self; + } + + public boolean export(Activity activity) { + if (isRunning()) { + return false; + } + + if (languages == null || languages.isEmpty()) { + Logger.d(LOG_TAG, "Nothing to do"); + return true; + } + + processThread = new Thread(() -> { for (Language l : languages) exportLanguage(activity, l); }); + processThread.start(); + + return true; + } + + public DictionaryExporter setLanguages(ArrayList languages) { + this.languages = languages; + return this; + } + + @Override + @NonNull + protected String generateFileName() { + return BASE_FILE_NAME + currentLanguage.getLocale().getLanguage() + "-" + System.currentTimeMillis() + FILE_EXTENSION; + } + + @Override + @NonNull + protected byte[] getWords(Activity activity) throws Exception { + SQLiteDatabase db = SQLiteOpener.getInstance(activity).getDb(); + if (db == null) { + throw new Exception("Could not open database"); + } + + return new ReadOps().getWords(db, currentLanguage, false).getBytes(); + } + + private void exportLanguage(Activity activity, Language language) { + currentLanguage = language; + if (currentLanguage == null) { + Logger.e(LOG_TAG, "Cannot export dictionary for null language"); + return; + } + + try { + long start = System.currentTimeMillis(); + write(activity); + sendSuccess(); + Logger.d(LOG_TAG, "All words for language: " + currentLanguage.getName() + " loaded. Time: " + (System.currentTimeMillis() - start) + "ms"); + } catch (Exception e) { + sendFailure(); + Logger.e(LOG_TAG, "Failed exporting dictionary for " + currentLanguage.getName() + " to: " + getOutputFile() + ". " + e); + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java index f41bdfaa..6202ae50 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java @@ -62,6 +62,31 @@ public class ReadOps { } + /** + * Gets all words as a ready-to-export CSV string. If the language is null or customWords is true, + * only custom words are returned. + */ + @NonNull + public String getWords(@NonNull SQLiteDatabase db, Language language, boolean customWords) { + StringBuilder words = new StringBuilder(); + + String table = customWords || language == null ? Tables.CUSTOM_WORDS : Tables.getWords(language.getId()); + String[] columns = customWords || language == null ? new String[]{"word", "langId"} : new String[]{"word", "frequency"}; + + try (Cursor cursor = db.query(table, columns, null, null, null, null, null)) { + while (cursor.moveToNext()) { + words + .append(cursor.getString(0)) + .append("\t") + .append(cursor.getInt(1)) + .append("\n"); + } + } + + return words.toString(); + } + + @NonNull public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { if (positions.isEmpty()) { @@ -69,7 +94,6 @@ public class ReadOps { return new WordList(); } - String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput); if (wordsQuery.isEmpty()) { return new WordList(); @@ -139,7 +163,6 @@ public class ReadOps { } - @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, Language language, String sequence, int generations) { try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null)) { return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString(); @@ -156,7 +179,6 @@ public class ReadOps { } - @NonNull private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) { StringBuilder sql = new StringBuilder("SELECT `start`, `end` FROM ") .append(Tables.getWordPositions(language.getId())) diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java index d35110a1..2bd3c26c 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java @@ -8,7 +8,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; -import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; @@ -16,7 +15,6 @@ import androidx.preference.PreferenceFragmentCompat; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; import io.github.sspanak.tt9.db.LegacyDb; import io.github.sspanak.tt9.db.WordStoreAsync; import io.github.sspanak.tt9.ime.helpers.InputModeValidator; @@ -31,7 +29,6 @@ import io.github.sspanak.tt9.preferences.screens.KeyPadScreen; import io.github.sspanak.tt9.preferences.screens.MainSettingsScreen; import io.github.sspanak.tt9.preferences.screens.SetupScreen; import io.github.sspanak.tt9.preferences.screens.UsageStatsScreen; -import io.github.sspanak.tt9.ui.DictionaryLoadingBar; public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { public SettingsStore settings; @@ -176,14 +173,4 @@ public class PreferencesActivity extends AppCompatActivity implements Preference Hotkeys.setDefault(settings); } } - - - public DictionaryLoadingBar getDictionaryProgressBar() { - return DictionaryLoadingBar.getInstance(this); - } - - - public DictionaryLoader getDictionaryLoader() { - return DictionaryLoader.getInstance(this); - } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemClickable.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemClickable.java index adfdbbce..9306347d 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemClickable.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemClickable.java @@ -1,9 +1,9 @@ package io.github.sspanak.tt9.preferences.items; +import androidx.annotation.NonNull; import androidx.preference.Preference; import java.util.ArrayList; -import java.util.List; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.preferences.SettingsStore; @@ -12,14 +12,43 @@ abstract public class ItemClickable { private long lastClickTime = 0; protected final Preference item; - private final ArrayList otherItems = new ArrayList<>(); - public ItemClickable(Preference item) { this.item = item; } + + public static void disableAll(@NonNull ArrayList items) { + for (ItemClickable i : items) { + i.disable(); + } + } + + + public static void enableAll(@NonNull ArrayList items) { + for (ItemClickable i : items) { + i.enable(); + } + } + + + public static void disableOthers(@NonNull ArrayList items, @NonNull ItemClickable exclude) { + for (ItemClickable i : items) { + if (i != exclude) { + i.disable(); + } + } + } + + + public static void enableAllClickHandlers(@NonNull ArrayList items) { + for (ItemClickable i : items) { + i.enableClickHandler(); + } + } + + public void disable() { item.setEnabled(false); } @@ -35,28 +64,6 @@ abstract public class ItemClickable { } - public ItemClickable setOtherItems(List others) { - otherItems.clear(); - otherItems.addAll(others); - return this; - } - - - protected void disableOtherItems() { - for (ItemClickable i : otherItems) { - i.disable(); - } - - } - - - protected void enableOtherItems() { - for (ItemClickable i : otherItems) { - i.enable(); - } - - } - protected boolean debounceClick(Preference p) { long now = System.currentTimeMillis(); if (now - lastClickTime < SettingsStore.PREFERENCES_CLICK_DEBOUNCE_TIME) { diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportAbstract.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportAbstract.java new file mode 100644 index 00000000..61c0bbe1 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportAbstract.java @@ -0,0 +1,93 @@ +package io.github.sspanak.tt9.preferences.items; + +import android.app.Activity; + +import androidx.preference.Preference; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.exporter.AbstractExporter; +import io.github.sspanak.tt9.ui.DictionaryNotification; + +abstract public class ItemExportAbstract extends ItemClickable { + final protected Activity activity; + final private Runnable onStart; + final private Runnable onFinish; + + public ItemExportAbstract(Preference item, Activity activity, Runnable onStart, Runnable onFinish) { + super(item); + this.activity = activity; + this.onStart = onStart; + this.onFinish = onFinish; + + AbstractExporter exporter = getExporter(); + exporter.setFailureHandler(() -> onFinishExporting(null)); + exporter.setSuccessHandler(this::onFinishExporting); + refreshStatus(); + } + + abstract protected AbstractExporter getExporter(); + + + public ItemExportAbstract refreshStatus() { + if (item != null) { + if (getExporter().isRunning()) { + setLoadingStatus(); + } else { + setReadyStatus(); + } + } + return this; + } + + + @Override + protected boolean onClick(Preference p) { + setLoadingStatus(); + if (!onStartExporting()) { + setReadyStatus(); + } + return true; + } + + + abstract protected boolean onStartExporting(); + + + protected void onFinishExporting(String outputFile) { + activity.runOnUiThread(() -> { + setReadyStatus(); + + if (outputFile == null) { + DictionaryNotification.getInstance(activity).showError( + activity.getString(R.string.dictionary_export_failed), + activity.getString(R.string.dictionary_export_failed_more_info) + ); + } else { + DictionaryNotification.getInstance(activity).showMessage( + activity.getString(R.string.dictionary_export_finished), + activity.getString(R.string.dictionary_export_finished_more_info, outputFile), + activity.getString(R.string.dictionary_export_finished_more_info, outputFile) + ); + } + }); + } + + + abstract protected String getLoadingMessage(); + + + protected void setLoadingStatus() { + onStart.run(); + disable(); + + String loadingMessage = getLoadingMessage(); + item.setSummary(loadingMessage); + DictionaryNotification.getInstance(activity).showLoadingMessage(loadingMessage, ""); + } + + + public void setReadyStatus() { + enable(); + onFinish.run(); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportCustomWords.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportCustomWords.java new file mode 100644 index 00000000..2bbe057e --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportCustomWords.java @@ -0,0 +1,42 @@ +package io.github.sspanak.tt9.preferences.items; + +import android.app.Activity; + +import androidx.preference.Preference; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.exporter.CustomWordsExporter; + +public class ItemExportCustomWords extends ItemExportAbstract { + final public static String NAME = "dictionary_export_custom"; + + + public ItemExportCustomWords(Preference item, Activity activity, Runnable onStart, Runnable onFinish) { + super(item, activity, onStart, onFinish); + } + + + @Override + protected CustomWordsExporter getExporter() { + return CustomWordsExporter.getInstance(); + } + + + protected boolean onStartExporting() { + return CustomWordsExporter.getInstance().export(activity); + } + + + @Override + protected String getLoadingMessage() { + return activity.getString(R.string.dictionary_export_generating_csv); + } + + public void setReadyStatus() { + super.setReadyStatus(); + item.setSummary(activity.getString( + R.string.dictionary_export_custom_words_summary, + CustomWordsExporter.getInstance().getOutputDir() + )); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportDictionary.java new file mode 100644 index 00000000..460a9348 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemExportDictionary.java @@ -0,0 +1,62 @@ +package io.github.sspanak.tt9.preferences.items; + +import android.app.Activity; + +import androidx.preference.Preference; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.exporter.DictionaryExporter; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.preferences.SettingsStore; + +public class ItemExportDictionary extends ItemExportAbstract { + final public static String NAME = "dictionary_export"; + + protected final SettingsStore settings; + + + public ItemExportDictionary(Preference item, Activity activity, SettingsStore settings, Runnable onStart, Runnable onFinish) { + super(item, activity, onStart, onFinish); + this.settings = settings; + } + + @Override + public ItemExportAbstract refreshStatus() { + if (item != null) { + item.setVisible(Logger.isDebugLevel()); + } + return super.refreshStatus(); + } + + @Override + protected DictionaryExporter getExporter() { + return DictionaryExporter.getInstance(); + } + + + protected boolean onStartExporting() { + return DictionaryExporter.getInstance() + .setLanguages(LanguageCollection.getAll(activity, settings.getEnabledLanguageIds())) + .export(activity); + } + + @Override + protected String getLoadingMessage() { + String message = activity.getString(R.string.dictionary_export_generating_csv); + + Language language = LanguageCollection.getLanguage(activity, settings.getInputLanguage()); + if (language != null) { + message = activity.getString(R.string.dictionary_export_generating_csv_for_language, language.getName()); + } + + return message; + } + + + public void setReadyStatus() { + super.setReadyStatus(); + item.setSummary(""); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java index 1313907b..7fa788f5 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java @@ -17,21 +17,27 @@ import io.github.sspanak.tt9.ui.UI; public class ItemLoadDictionary extends ItemClickable { - public static final String NAME = "dictionary_load"; + public final static String NAME = "dictionary_load"; private final Context context; private final SettingsStore settings; + private final Runnable onStart; + private final Runnable onFinish; + private final DictionaryLoader loader; private final DictionaryLoadingBar progressBar; - public ItemLoadDictionary(Preference item, Context context, SettingsStore settings, DictionaryLoader loader, DictionaryLoadingBar progressBar) { + public ItemLoadDictionary(Preference item, Context context, SettingsStore settings, Runnable onStart, Runnable onFinish) { super(item); this.context = context; - this.loader = loader; - this.progressBar = progressBar; + this.loader = DictionaryLoader.getInstance(context); + this.progressBar = DictionaryLoadingBar.getInstance(context); this.settings = settings; + this.onStart = onStart; + this.onFinish = onFinish; + loader.setOnStatusChange(this::onLoadingStatusChange); refreshStatus(); @@ -56,7 +62,7 @@ public class ItemLoadDictionary extends ItemClickable { } else if (progressBar.isFailed()) { setReadyStatus(); UI.toastFromAsync(context, progressBar.getMessage()); - } else if (progressBar.isCompleted()) { + } else if (!progressBar.inProgress()) { setReadyStatus(); UI.toastFromAsync(context, R.string.dictionary_loaded); } @@ -78,13 +84,13 @@ public class ItemLoadDictionary extends ItemClickable { private void setLoadingStatus() { - disableOtherItems(); + onStart.run(); item.setTitle(context.getString(R.string.dictionary_cancel_load)); } private void setReadyStatus() { - enableOtherItems(); + onFinish.run(); item.setTitle(context.getString(R.string.dictionary_load_title)); item.setSummary(progressBar.isFailed() || progressBar.isCancelled() ? progressBar.getMessage() : ""); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java index 37cae994..9db54553 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import io.github.sspanak.tt9.R; import io.github.sspanak.tt9.db.WordStoreAsync; -import io.github.sspanak.tt9.db.DictionaryLoader; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; @@ -17,22 +16,20 @@ public class ItemTruncateAll extends ItemClickable { public static final String NAME = "dictionary_truncate"; protected final PreferencesActivity activity; - protected final DictionaryLoader loader; + private final Runnable onStart; + private final Runnable onFinish; - public ItemTruncateAll(Preference item, PreferencesActivity activity, DictionaryLoader loader) { + public ItemTruncateAll(Preference item, PreferencesActivity activity, Runnable onStart, Runnable onFinish) { super(item); this.activity = activity; - this.loader = loader; + this.onStart = onStart; + this.onFinish = onFinish; } @Override protected boolean onClick(Preference p) { - if (loader != null && loader.isRunning()) { - return false; - } - onStartDeleting(); ArrayList languageIds = new ArrayList<>(); for (Language lang : LanguageCollection.getAll(activity, false)) { @@ -45,7 +42,7 @@ public class ItemTruncateAll extends ItemClickable { protected void onStartDeleting() { - disableOtherItems(); + onStart.run(); disable(); item.setSummary(R.string.dictionary_truncating); } @@ -53,7 +50,7 @@ public class ItemTruncateAll extends ItemClickable { protected void onFinishDeleting() { activity.runOnUiThread(() -> { - enableOtherItems(); + onFinish.run(); item.setSummary(""); enable(); UI.toastFromAsync(activity, R.string.dictionary_truncated); diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java index bdb05158..15f2ffb2 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java @@ -5,7 +5,6 @@ import androidx.preference.Preference; import java.util.ArrayList; import io.github.sspanak.tt9.db.WordStoreAsync; -import io.github.sspanak.tt9.db.DictionaryLoader; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; @@ -18,19 +17,14 @@ public class ItemTruncateUnselected extends ItemTruncateAll { private final SettingsStore settings; - public ItemTruncateUnselected(Preference item, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) { - super(item, context, loader); + public ItemTruncateUnselected(Preference item, PreferencesActivity context, SettingsStore settings, Runnable onStart, Runnable onFinish) { + super(item, context, onStart, onFinish); this.settings = settings; } - @Override protected boolean onClick(Preference p) { - if (loader != null && loader.isRunning()) { - return false; - } - ArrayList unselectedLanguageIds = new ArrayList<>(); ArrayList selectedLanguageIds = settings.getEnabledLanguageIds(); for (Language lang : LanguageCollection.getAll(activity, false)) { diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/DictionariesScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/DictionariesScreen.java index a3ca729c..aaa31f53 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/DictionariesScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/DictionariesScreen.java @@ -1,18 +1,28 @@ package io.github.sspanak.tt9.preferences.screens; -import java.util.Arrays; +import java.util.ArrayList; import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.db.exporter.CustomWordsExporter; +import io.github.sspanak.tt9.db.exporter.DictionaryExporter; import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.preferences.items.ItemClickable; +import io.github.sspanak.tt9.preferences.items.ItemExportCustomWords; +import io.github.sspanak.tt9.preferences.items.ItemExportDictionary; import io.github.sspanak.tt9.preferences.items.ItemLoadDictionary; import io.github.sspanak.tt9.preferences.items.ItemSelectLanguage; import io.github.sspanak.tt9.preferences.items.ItemTruncateAll; import io.github.sspanak.tt9.preferences.items.ItemTruncateUnselected; public class DictionariesScreen extends BaseScreenFragment { - final public static String NAME = "Dictionaries"; + public static final String NAME = "Dictionaries"; + + private final ArrayList clickables = new ArrayList<>(); private ItemLoadDictionary loadItem; + private ItemExportDictionary exportDictionaryItem; + private ItemExportCustomWords exportCustomWordsItem; public DictionariesScreen() { init(); } public DictionariesScreen(PreferencesActivity activity) { init(activity); } @@ -30,35 +40,78 @@ public class DictionariesScreen extends BaseScreenFragment { ); multiSelect.populate().enableValidation(); - loadItem = new ItemLoadDictionary( - findPreference(ItemLoadDictionary.NAME), + loadItem = new ItemLoadDictionary(findPreference(ItemLoadDictionary.NAME), activity, activity.settings, - activity.getDictionaryLoader(), - activity.getDictionaryProgressBar() + () -> ItemClickable.disableOthers(clickables, loadItem), + this::onActionFinish ); - ItemTruncateUnselected deleteItem = new ItemTruncateUnselected( + exportDictionaryItem = new ItemExportDictionary(findPreference(ItemExportDictionary.NAME), + activity, + activity.settings, + this::onActionStart, + this::onActionFinish + ); + + clickables.add(loadItem); + clickables.add(exportDictionaryItem); + + clickables.add(new ItemTruncateUnselected( findPreference(ItemTruncateUnselected.NAME), activity, activity.settings, - activity.getDictionaryLoader() - ); + this::onActionStart, + this::onActionFinish + )); - ItemTruncateAll truncateItem = new ItemTruncateAll( + clickables.add(new ItemTruncateAll( findPreference(ItemTruncateAll.NAME), activity, - activity.getDictionaryLoader() - ); + this::onActionStart, + this::onActionFinish + )); - loadItem.setOtherItems(Arrays.asList(truncateItem, deleteItem)).enableClickHandler(); - deleteItem.setOtherItems(Arrays.asList(truncateItem, loadItem)).enableClickHandler(); - truncateItem.setOtherItems(Arrays.asList(deleteItem, loadItem)).enableClickHandler(); + exportCustomWordsItem = new ItemExportCustomWords( + findPreference(ItemExportCustomWords.NAME), + activity, + this::onActionStart, + this::onActionFinish); + + clickables.add(exportCustomWordsItem); + + ItemClickable.enableAllClickHandlers(clickables); + refreshItems(); } @Override public void onResume() { super.onResume(); + refreshItems(); + } + + + private void refreshItems() { loadItem.refreshStatus(); + exportDictionaryItem.refreshStatus(); + exportCustomWordsItem.refreshStatus(); + + if (DictionaryLoader.getInstance(activity).isRunning()) { + loadItem.refreshStatus(); + ItemClickable.disableOthers(clickables, loadItem); + } else if (CustomWordsExporter.getInstance().isRunning() || DictionaryExporter.getInstance().isRunning()) { + onActionStart(); + } else { + onActionFinish(); + } + } + + + private void onActionStart() { + ItemClickable.disableAll(clickables); + } + + private void onActionFinish() { + ItemClickable.enableAll(clickables); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryLoadingBar.java b/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryLoadingBar.java index d43b78b1..5e5e4b77 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryLoadingBar.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryLoadingBar.java @@ -1,16 +1,8 @@ package io.github.sspanak.tt9.ui; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.os.Build; import android.os.Bundle; -import androidx.core.app.NotificationCompat; - import java.io.FileNotFoundException; import java.io.IOException; import java.util.Locale; @@ -21,28 +13,15 @@ import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException; import io.github.sspanak.tt9.languages.InvalidLanguageException; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; -import io.github.sspanak.tt9.preferences.PreferencesActivity; -import io.github.sspanak.tt9.preferences.screens.DictionariesScreen; -public class DictionaryLoadingBar { +public class DictionaryLoadingBar extends DictionaryNotification { private static DictionaryLoadingBar self; - private static final int NOTIFICATION_ID = 1; - private static final String NOTIFICATION_CHANNEL_ID = "loading-notifications"; - - private final NotificationManager manager; - private final NotificationCompat.Builder notificationBuilder; - private final Resources resources; private boolean isStopped = false; private boolean hasFailed = false; - private int maxProgress = 0; - private int progress = 0; - private String title = ""; - private String message = ""; - public static DictionaryLoadingBar getInstance(Context context) { if (self == null) { @@ -54,39 +33,17 @@ public class DictionaryLoadingBar { private DictionaryLoadingBar(Context context) { - resources = context.getResources(); - - manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = getNotificationBuilderCompat(context); - - notificationBuilder - .setContentIntent(createNavigationIntent(context)) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setOnlyAlertOnce(true); + super(context); } - private PendingIntent createNavigationIntent(Context context) { - Intent intent = new Intent(context, PreferencesActivity.class); - intent.putExtra("screen", DictionariesScreen.NAME); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return PendingIntent.getActivity(context, 0, intent,PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + public String getMessage() { + return message; } - private NotificationCompat.Builder getNotificationBuilderCompat(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - manager.createNotificationChannel(new NotificationChannel( - NOTIFICATION_CHANNEL_ID, - "Dictionary Status", - NotificationManager.IMPORTANCE_LOW - )); - return new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); - } else { - //noinspection deprecation - return new NotificationCompat.Builder(context); - } + public String getTitle() { + return title; } @@ -100,26 +57,11 @@ public class DictionaryLoadingBar { } - public boolean isCompleted() { - return progress >= maxProgress; - } - - public boolean isFailed() { return hasFailed; } - public String getTitle() { - return title; - } - - - public String getMessage() { - return message; - } - - public void show(Context context, Bundle data) { String error = data.getString("error", null); int fileCount = data.getInt("fileCount", -1); @@ -185,7 +127,7 @@ public class DictionaryLoadingBar { message = currentFileProgress + "%"; } - renderProgress(); + renderMessage(); } @@ -207,37 +149,4 @@ public class DictionaryLoadingBar { renderError(); } - - - private void hide() { - progress = maxProgress = 0; - manager.cancel(NOTIFICATION_ID); - } - - - private void renderError() { - NotificationCompat.BigTextStyle bigMessage = new NotificationCompat.BigTextStyle(); - bigMessage.setBigContentTitle(title); - bigMessage.bigText(message); - - notificationBuilder - .setSmallIcon(android.R.drawable.stat_notify_error) - .setStyle(bigMessage) - .setOngoing(false) - .setProgress(maxProgress, progress, false); - - manager.notify(NOTIFICATION_ID, notificationBuilder.build()); - } - - - private void renderProgress() { - notificationBuilder - .setSmallIcon(isCompleted() ? R.drawable.ic_done : android.R.drawable.stat_notify_sync) - .setOngoing(!isCompleted()) - .setProgress(maxProgress, progress, false) - .setContentTitle(title) - .setContentText(message); - - manager.notify(NOTIFICATION_ID, notificationBuilder.build()); - } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryNotification.java b/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryNotification.java new file mode 100644 index 00000000..95e56cf4 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/DictionaryNotification.java @@ -0,0 +1,154 @@ +package io.github.sspanak.tt9.ui; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.preferences.screens.DictionariesScreen; + +public abstract class DictionaryNotification { + private static DictionaryNotification self; + private static final int NOTIFICATION_ID = 1; + private static final String NOTIFICATION_CHANNEL_ID = "dictionary-notifications"; + + private final NotificationManager manager; + private final NotificationCompat.Builder notificationBuilder; + protected final Resources resources; + + protected int maxProgress = 0; + protected int progress = 0; + protected boolean indeterminate = false; + protected String title = ""; + protected String message = ""; + protected String messageLong = ""; + + + protected DictionaryNotification(Context context) { + resources = context.getResources(); + + manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = getNotificationBuilderCompat(context); + + notificationBuilder + .setContentIntent(createNavigationIntent(context)) + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setOnlyAlertOnce(true); + } + + + public static DictionaryNotification getInstance(Context context) { + if (self == null) { + self = new DictionaryNotification(context) { + }; + } + return self; + } + + + private PendingIntent createNavigationIntent(Context context) { + Intent intent = new Intent(context, PreferencesActivity.class); + intent.putExtra("screen", DictionariesScreen.NAME); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return PendingIntent.getActivity(context, 0, intent,PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + + private NotificationCompat.Builder getNotificationBuilderCompat(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + manager.createNotificationChannel(new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Dictionary Status", + NotificationManager.IMPORTANCE_LOW + )); + return new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); + } else { + //noinspection deprecation + return new NotificationCompat.Builder(context); + } + } + + + public void showMessage(@NonNull String title, @NonNull String message, @NonNull String messageLong) { + progress = maxProgress = 0; + indeterminate = false; + this.title = title; + this.message = message; + this.messageLong = messageLong; + renderMessage(); + } + + + public void showLoadingMessage(@NonNull String title, @NonNull String message) { + this.title = title; + this.message = message; + messageLong = ""; + indeterminate = true; + progress = 1; + maxProgress = 2; + renderMessage(); + } + + + public void showError(@NonNull String title, @NonNull String message) { + progress = maxProgress = 0; + indeterminate = false; + this.title = title; + this.message = message; + renderError(); + } + + + protected void hide() { + progress = maxProgress = 0; + manager.cancel(NOTIFICATION_ID); + } + + + public boolean inProgress() { + return progress < maxProgress; + } + + + protected void renderError() { + NotificationCompat.BigTextStyle bigMessage = new NotificationCompat.BigTextStyle(); + bigMessage.setBigContentTitle(title); + bigMessage.bigText(message); + + notificationBuilder + .setSmallIcon(android.R.drawable.stat_notify_error) + .setContentTitle(title) + .setContentText(message) + .setOngoing(false) + .setStyle(bigMessage) + .setProgress(maxProgress, progress, false); + + manager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + + + protected void renderMessage() { + NotificationCompat.BigTextStyle bigMessage = new NotificationCompat.BigTextStyle(); + bigMessage.setBigContentTitle(title); + bigMessage.bigText(messageLong.isEmpty() ? message : messageLong); + + notificationBuilder + .setSmallIcon(inProgress() ? android.R.drawable.stat_notify_sync : R.drawable.ic_done) + .setOngoing(inProgress()) + .setProgress(maxProgress, progress, indeterminate) + .setStyle(bigMessage) + .setContentTitle(title) + .setContentText(message); + + manager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } +} diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 37102b66..b5a980c0 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -34,6 +34,11 @@ Бутони на екрана Назад Зелена слушалка + Експортирай избраните + Експортирай + Експортиране на CSV с всички добавени думи в: „%1$s“. + Неуспешно експортиране + За повече информация, активирайте режима за отстраняване на грешки и прегледайте журнала. Налично е обновление на речника за „%1$s“. Искате ли да го заредите? Зареди Дарете @@ -86,4 +91,8 @@ Да се добави ли „%1$s“ към %2$s? Защита от случайно повтарящи бутони Изключена + Експортирането завърши + Думите са експортирани в: „%1$s“. + Експортиране на CSV… + Експортиране на CSV (%1$s)… diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bbf00575..6c0b61b1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -31,6 +31,11 @@ Ja Nein Automatisch + Ausgewählte exportieren + Exportieren + Exportiere ein CSV mit allen hinzugefügten Wörtern nach: „%1$s“. + Export fehlgeschlagen + Für weitere Informationen, aktivieren Sie den Debug-Modus und sehen Sie sich die Protokolle an. " Wörterbuchupdate verfügbar für „%1$s“. Möchten Sie es laden?" Laden Spenden @@ -39,4 +44,8 @@ Nachrichten mit \"OK\" in Google Chat senden Schutz vor versehentlichem Tastenwiederholen Aus + Export abgeschlossen + Wörter exportiert nach: „%1$s“. + CSV wird exportiert… + CSV wird exportiert (%1$s)… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e7388fe7..df486abc 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -74,6 +74,11 @@ No Automática + Exportar seleccionados + Exportar + Exportar un CSV con todas las palabras añadidas a: \"%1$s\". + Fallo en la exportación + Para obtener más información, habilita el modo de depuración y consulta los registros. Actualización del diccionario disponible para «%1$s». ¿Te gustaría cargarlo? Cargar Donar @@ -82,4 +87,8 @@ Enviar mensajes con «OK» en Google Chat Protección contra la repetición accidental de teclas Apagado + " Exportación completada" + Palabras exportadas a: \"%1$s\". + Exportando CSV… + Exportando CSV (%1$s)… diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0c23c456..423a6b38 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -74,10 +74,19 @@ Non Automatique Ajouter mot « %1$s » à %2$s? + Exporter les sélectionées + Exporter + Exporter un CSV avec tous les mots ajoutés vers : «%1$s». + Échec de l\'exportation + Pour plus d\'informations, activez le mode de débogage et consultez les journaux. Mise à jour du dictionnaire «%1$s» disponible. Souhaitez-vous le charger ? Charger Donner Si vous aimez %1$s vous pouvez soutenir son développement à : %2$s Protection contre la répétition accidentelle des touches Désactivée + Exportation terminée + Mots exportés vers : «%1$s». + Exportation CSV en cours… + Exportation CSV en cours (%1$s)… diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a8a99d51..3a3e2620 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -40,6 +40,11 @@ Si No Automatica + Esporta selezionate + Esportare + Esporta un CSV con tutte le parole aggiunte su: \"%1$s\". + Esportazione fallita + Per ulteriori informazioni, abilita la modalità di debug e consulta i log. Aggiornamento del dizionario disponibile per \"%1$s\". Vuoi caricarlo? Carica Donare @@ -48,5 +53,9 @@ Inviare messaggi con \"OK\" su Google Chat Protezione contro la ripetizione accidentale dei tasti Spento + Esportazione completata + Parole esportate su: \"%1$s\". + CSV in corso… + CSV in corso (%1$s)… diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 68b000cd..abdc1985 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -67,6 +67,11 @@ כן לא אוטומטי + ייצוא שנבחר + לְיְצוֹא + ייצוא CSV עם כל המילים שנוספו ל: \"%1$s\". + נכשל בייצוא + "למידע נוסף, הפעל מצב איתור באגים וראה את הלוגים. " עדכון מילון זמין עבור \"%1$s\". האם תרצה לטעון אותו? טען לִתְרוֹם @@ -75,4 +80,8 @@ שלח הודעות עם \"OK\" ב-Google Chat הגנה מפני חזרת מפתח בשוגג כבוי + הייצוא הושלם + המילים יוצאות ל: \"%1$s\". + מייצא CSV… + מייצא CSV (%1$s)... diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 505f4bc3..79d89c9d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -31,6 +31,11 @@ Ja Nee Automatisch + Geselecteerde exporteren + Exporteren + Exporteer een CSV met alle toegevoegde woorden naar: \"%1$s\". + Exporteren mislukt + Voor meer informatie, schakel de debug-modus in en bekijk de logs. Woordenboekupdate beschikbaar voor \"%1$s\". Wil je het laden? Laden Doneer @@ -39,4 +44,8 @@ Stuur berichten met \"OK\" in Google Chat Bescherming tegen het per ongeluk herhalen van toetsen Uit + Export voltooid + Woorden geëxporteerd naar: \"%1$s\". + CSV exporteren… + CSV exporteren (%1$s)… diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e2853346..6e75f915 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -63,6 +63,11 @@ Sim Não Automático + Exportar selecionados + Exportar + Exportar um CSV com todas as palavras adicionadas para: \"%1$s\". + Falha na exportação + Para mais informações, ative o modo de depuração e veja os registros. Atualização do dicionário disponível para \"%1$s\". Você gostaria de carregá-lo? Carregar Doar @@ -71,4 +76,8 @@ Enviar mensagens com \"OK\" no Google Chat Proteção contra repetição acidental de teclas Desligado + Exportação concluída + Palavras exportadas para: \"%1$s\". + Exportando CSV… + Exportando CSV (%1$s)… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 39c08606..e3444dfa 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -79,6 +79,11 @@ Нет Автоматически Добавить слово «%1$s» в %2$s? + Экспорт выбранные + Экспортировать + Экспорт CSV со всеми добавленными словами в: «%1$s». + Ошибка экспорта + Для получения дополнительной информации включите режим отладки и просмотрите журналы. Доступно обновление словаря для «%1$s». Хотите загрузить его? Загрузить Поддержать @@ -86,4 +91,8 @@ Отправка сообщения с «ОК» в Google Chat Защита от случайного повторения нажатий Выключена + Экспорт завершен + Слова экспортированы в: «%1$s». + Экспорт CSV… + Экспорт CSV (%1$s)… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2b21ec72..90cbfef9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -64,6 +64,11 @@ Словник видалено. Видаляється… + Експорт вибрані + Експортувати + Експорт CSV з усіма доданими словами в: \"%1$s\". + Помилка експорту + Для отримання додаткової інформації увімкніть режим відлагодження та перегляньте журнали. Доступне оновлення словника для \"%1$s\". Бажаєте його завантажити? Завантажити @@ -97,4 +102,8 @@ Новий рядок Пробіл + Експорт завершено + Слова експортовані в: \" %1$s \". + Експорт CSV… + Експорт CSV (%1$s)… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16503f5a..f2a7ef59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ About ABC Mode + Added Words Compatibility Appearance Debug Options @@ -71,6 +72,16 @@ Dictionary successfully cleared. Deleting… + Export Selected + Export + Export a CSV with all added words in: \"%1$s\". + Exporting Failed + For more info, enable debugging mode and see the logs. + Exporting Finished + Words exported to: \"%1$s\". + Exporting CSV… + Exporting CSV (%1$s)… + Dictionary update available for \"%1$s\". Would you like to load it? Load diff --git a/app/src/main/res/xml/prefs_screen_dictionaries.xml b/app/src/main/res/xml/prefs_screen_dictionaries.xml index 923db205..19fb56c9 100644 --- a/app/src/main/res/xml/prefs_screen_dictionaries.xml +++ b/app/src/main/res/xml/prefs_screen_dictionaries.xml @@ -11,6 +11,12 @@ app:layout="@layout/pref_text" app:title="@string/dictionary_load_title" /> + + + + + + + + diff --git a/docs/user-manual.md b/docs/user-manual.md index 4729fc31..748a9581 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -10,28 +10,9 @@ _If you don't see the icon right after installing, restart your phone and it sho If your phone does not have a hardware keypad, check out the [On-screen Keypad section](#on-screen-keypad). ### Enabling Predictive Mode -Predictive Mode requires a language dictionary to be loaded in order provide word suggestions. You can toggle the enabled languages and load their dictionaries from: [Settings screen](#settings-screen) → Languages. +Predictive Mode requires a language dictionary to be loaded in order provide word suggestions. You can toggle the enabled languages and load their dictionaries from: Settings Screen → [Languages](#language-options). In case you have forgotten to load some dictionary, Traditional T9 will do it for you automatically, when you start typing. -In case you have forgotten to load some dictionary, Traditional T9 will do it for you automatically. Just go to some application where you can type and it will start loading. You will be prompted to wait until it completes and after that, you can start typing right away. - -_If you [delete one or more dictionaries](#deleting-a-dictionary), they will NOT reload automatically. You will have to do so manually. Only dictionaries for newly enabled languages will load automatically._ - -### Dictionary Tips - -#### Loading a Dictionary -Once a dictionary is loaded, it will stay there until you use one of the "delete" options. This means you can enable and disable languages without reloading their dictionaries every time. Just do it once, only the first time. - -It also means that if you need to start using language X, you can safely disable all other languages, load only dictionary X (and save time!), then re-enable all languages you used before. - -Have in mind reloading a dictionary will reset the suggestion popularity to the factory defaults. However, there should be nothing to worry about. For the most part, you will see little to no difference in the suggestion order, unless you oftenly use uncommon words. - -#### Deleting a Dictionary - -If you have stopped using languages X or Y, you could disable them and also use "Delete Unselected", to free some storage space. - -To delete everything, regardless of the selection, use "Delete All". - -In all cases, your custom added words will be preserved and restored once you reload the respective dictionary. +For more information, [see below](#language-options). ## Hotkeys @@ -156,6 +137,33 @@ Click on the Traditional T9 launcher icon. _The actual menu names may vary depending on your phone, Android version and language._ +### Language Options + +#### Loading a Dictionary +After enabling one or more new languages, you must load the respective dictionaries for Predictive Mode. Once a dictionary is loaded, it will stay there until you use one of the "delete" options. This means you can enable and disable languages without reloading their dictionaries every time. Just do it once, only the first time. + +It also means that if you need to start using language X, you can safely disable all other languages, load only dictionary X (and save time!), then re-enable all languages you used before. + +Have in mind reloading a dictionary will reset the suggestion popularity to the factory defaults. However, there should be nothing to worry about. For the most part, you will see little to no difference in the suggestion order, unless you oftenly use uncommon words. + +#### Automatic Dictionary Loading + +If you skip or forget to load a dictionary from the Settings screen, it will happen automatically later, when you go to an application where you can type, and switch to Predictive Mode. You will be prompted to wait until it completes and after that, you can start typing right away. + +If you delete one or more dictionaries, they will NOT reload automatically. You will have to do so manually. Only dictionaries for newly enabled languages will load automatically. + +#### Deleting a Dictionary +If you have stopped using languages X or Y, you could disable them and also use "Delete Unselected", to free some storage space. + +To delete everything, regardless of the selection, use "Delete All". + +In all cases, your custom added words will be preserved and restored once you reload the respective dictionary. + +#### Added Words +The "Export" option allows you to export all added words, for all languages, including any added emoji, to a CSV file. Then, you can use the CSV file to make Traditional T9 better! Go to Github and share the words in a [new issue](https://github.com/sspanak/tt9/issues) or [pull request](https://github.com/sspanak/tt9/issues). After being reviewed and approved, they will be included in the next version. + +Using "Delete", you can search for and delete misspelled words or others that you don't want in the dictionary. + ### Compatibility Options & Troubleshooting For a number of applications or devices, it is possible to enable special options, which will make Traditional T9 work better with them. You can find them in: Settings → Initial Setup, under the Compatibility section. @@ -165,7 +173,7 @@ On some devices, in Predictive Mode, you may not be able to see all suggestions, #### Key repeat protection CAT S22 Flip and Qin F21 phones are known for their low quality keypads, which degrade quickly over time, and start registering multiple clicks for a single key press. You may notice this when typing or navigating the phone menus. -For CAT phones the recommended setting is 75-100 ms. For Qin F21, try with 20-30 ms. If you are still experiencing the issue, increase the value a bit, but generally try to keep it as low as possible. +For CAT phones the recommended setting is 50-75 ms. For Qin F21, try with 20-30 ms. If you are still experiencing the issue, increase the value a bit, but generally try to keep it as low as possible. _**Note:** The higher value you set, the slower you will have to type. TT9 will ignore very quick key presses._