diff --git a/app/src/main/java/io/github/sspanak/tt9/db/exporter/AbstractExporter.java b/app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractExporter.java similarity index 72% rename from app/src/main/java/io/github/sspanak/tt9/db/exporter/AbstractExporter.java rename to app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractExporter.java index e1f540cb..1d952bf0 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/exporter/AbstractExporter.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractExporter.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db.exporter; +package io.github.sspanak.tt9.db.customWords; import android.app.Activity; import android.content.ContentResolver; @@ -16,19 +16,13 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import io.github.sspanak.tt9.util.ConsumerCompat; import io.github.sspanak.tt9.util.Permissions; -public abstract class AbstractExporter { +public abstract class AbstractExporter extends AbstractFileProcessor { protected static String FILE_EXTENSION = ".csv"; protected static String MIME_TYPE = "text/csv"; - protected Runnable failureHandler; - protected Runnable startHandler; - protected ConsumerCompat successHandler; - private Thread processThread; private String outputFile; - private String statusMessage = ""; private void writeAndroid10(Activity activity) throws Exception { @@ -107,18 +101,6 @@ public abstract class AbstractExporter { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? Environment.DIRECTORY_DOCUMENTS : Environment.DIRECTORY_DOWNLOADS; } - protected void sendFailure() { - if (failureHandler != null) { - failureHandler.run(); - } - } - - protected void sendStart(@NonNull String message) { - if (startHandler != null) { - statusMessage = message; - startHandler.run(); - } - } protected void sendSuccess() { if (successHandler != null) { @@ -126,39 +108,7 @@ public abstract class AbstractExporter { } } - public boolean export(@NonNull Activity activity) { - if (isRunning()) { - return false; - } - processThread = new Thread(() -> exportSync(activity)); - processThread.start(); - - return true; - } - - public boolean isRunning() { - return processThread != null && processThread.isAlive(); - } - - public String getStatusMessage() { - return statusMessage; - } - - public void setFailureHandler(Runnable handler) { - failureHandler = handler; - } - - public void setStartHandler(Runnable handler) { - startHandler = handler; - } - - public void setSuccessHandler(ConsumerCompat handler) { - successHandler = handler; - } - - - abstract protected void exportSync(Activity activity); @NonNull abstract protected String generateFileName(); @NonNull abstract protected byte[] getFileContents(Activity activity) throws Exception; } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractFileProcessor.java b/app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractFileProcessor.java new file mode 100644 index 00000000..208318a6 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/AbstractFileProcessor.java @@ -0,0 +1,63 @@ +package io.github.sspanak.tt9.db.customWords; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.util.ConsumerCompat; + +abstract public class AbstractFileProcessor { + protected Runnable failureHandler; + protected Runnable startHandler; + protected ConsumerCompat successHandler; + + private Thread processThread; + protected String statusMessage = ""; + + public boolean isRunning() { + return processThread != null && processThread.isAlive(); + } + + public String getStatusMessage() { + return statusMessage; + } + + protected void sendFailure() { + if (failureHandler != null) { + failureHandler.run(); + } + } + + protected void sendStart(@NonNull String message) { + if (startHandler != null) { + statusMessage = message; + startHandler.run(); + } + } + + public void setFailureHandler(Runnable handler) { + failureHandler = handler; + } + + public void setStartHandler(Runnable handler) { + startHandler = handler; + } + + public void setSuccessHandler(ConsumerCompat handler) { + successHandler = handler; + } + + public boolean run(@NonNull Activity activity) { + if (isRunning()) { + return false; + } + + processThread = new Thread(() -> runSync(activity)); + processThread.start(); + + return true; + } + + abstract protected void sendSuccess(); + abstract protected void runSync(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/customWords/CustomWordsExporter.java similarity index 93% rename from app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java rename to app/src/main/java/io/github/sspanak/tt9/db/customWords/CustomWordsExporter.java index bf771331..708b6335 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/exporter/CustomWordsExporter.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/CustomWordsExporter.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db.exporter; +package io.github.sspanak.tt9.db.customWords; import android.app.Activity; import android.database.sqlite.SQLiteDatabase; @@ -23,7 +23,7 @@ public class CustomWordsExporter extends AbstractExporter { } @Override - protected void exportSync(Activity activity) { + protected void runSync(Activity activity) { try { sendStart(activity.getString(R.string.dictionary_export_generating_csv)); write(activity); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/customWords/CustomWordsImporter.java b/app/src/main/java/io/github/sspanak/tt9/db/customWords/CustomWordsImporter.java new file mode 100644 index 00000000..cb5ad7e7 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/CustomWordsImporter.java @@ -0,0 +1,213 @@ +package io.github.sspanak.tt9.db.customWords; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; + +import androidx.annotation.NonNull; + +import java.io.BufferedReader; +import java.io.IOException; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.entities.CustomWord; +import io.github.sspanak.tt9.db.entities.CustomWordFile; +import io.github.sspanak.tt9.db.sqlite.InsertOps; +import io.github.sspanak.tt9.db.sqlite.ReadOps; +import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.ConsumerCompat; +import io.github.sspanak.tt9.util.Logger; +import io.github.sspanak.tt9.util.Timer; + +public class CustomWordsImporter extends AbstractFileProcessor { + private static CustomWordsImporter self; + + private ConsumerCompat progressHandler; + private ConsumerCompat failureHandler; + + private final Context context; + private CustomWordFile file; + private final Resources resources; + private SQLiteOpener sqlite; + + private long lastProgressUpdate = 0; + + + public static CustomWordsImporter getInstance(Context context) { + if (self == null) { + self = new CustomWordsImporter(context); + } + + return self; + } + + + private CustomWordsImporter(Context context) { + super(); + this.context = context; + this.resources = context.getResources(); + } + + + public void setProgressHandler(ConsumerCompat handler) { + progressHandler = handler; + } + + + public void setFailureHandler(ConsumerCompat handler) { + failureHandler = handler; + } + + + private void sendProgress(int progress) { + long now = System.currentTimeMillis(); + if (lastProgressUpdate + SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME < now) { + progressHandler.accept(progress); + lastProgressUpdate = now; + } + } + + + @Override + protected void sendSuccess() { + if (successHandler != null) { + successHandler.accept(file.getName()); + } + } + + + private void sendFailure(String errorMessage) { + if (failureHandler != null) { + failureHandler.accept(errorMessage); + } + } + + + public boolean run(@NonNull Activity activity, @NonNull CustomWordFile file) { + this.file = file; + return super.run(activity); + } + + + @Override + protected void runSync(Activity activity) { + Timer.start(getClass().getSimpleName()); + + sendStart(resources.getString(R.string.dictionary_import_running)); + if (isFileValid() && isThereRoomForMoreWords() && insertWords()) { + sendSuccess(); + Logger.i(getClass().getSimpleName(), "Imported " + file.getName() + " in " + Timer.get(getClass().getSimpleName()) + " ms"); + } else { + Logger.e(getClass().getSimpleName(), "Failed to import " + file.getName()); + } + } + + + private boolean openDb() { + sqlite = SQLiteOpener.getInstance(context); + if (sqlite.getDb() != null) { + return true; + } + + Logger.e(getClass().getSimpleName(), "Could not open database"); + sendFailure(resources.getString(R.string.dictionary_import_failed)); + return false; + } + + + private boolean insertWords() { + ReadOps readOps = new ReadOps(); + int ignoredWords = 0; + int lineCount = 1; + + try (BufferedReader reader = file.getReader()) { + sqlite.beginTransaction(); + + for (String line; (line = reader.readLine()) != null; lineCount++) { + if (!isLineCountValid(lineCount)) { + sqlite.failTransaction(); + return false; + } + + CustomWord customWord = createCustomWord(line, lineCount); + if (customWord == null) { + sqlite.failTransaction(); + return false; + } + + if (readOps.exists(sqlite.getDb(), customWord.language, customWord.word)) { + ignoredWords++; + } else { + InsertOps.insertCustomWord(sqlite.getDb(), customWord.language, customWord.sequence, customWord.word); + } + + if (file.getSize() > 20) { + sendProgress(lineCount * 100 / file.getSize()); + } + } + + sqlite.finishTransaction(); + } catch (IOException e) { + sqlite.failTransaction(); + Logger.e(getClass().getSimpleName(), "Error opening the file. " + e.getMessage()); + sendFailure(resources.getString(R.string.dictionary_import_error_cannot_read_file)); + return false; + } + + if (ignoredWords > 0) { + Logger.i(getClass().getSimpleName(), "Skipped " + ignoredWords + " word(s) that are already in the dictionary."); + } + + return true; + } + + + private boolean isFileValid() { + if (file != null) { + return true; + } + + Logger.e(getClass().getSimpleName(), "Can not read a NULL file"); + sendFailure(resources.getString(R.string.dictionary_import_error_cannot_read_file)); + return false; + } + + + private boolean isThereRoomForMoreWords() { + if (!openDb()) { + return false; + } + + if ((new ReadOps()).countCustomWords(sqlite.getDb()) > SettingsStore.CUSTOM_WORDS_MAX) { + sendFailure(resources.getString(R.string.dictionary_import_error_too_many_words)); + return false; + } + + return true; + } + + + private boolean isLineCountValid(int lineCount) { + if (lineCount <= SettingsStore.CUSTOM_WORDS_IMPORT_MAX_LINES) { + return true; + } + + sendFailure(resources.getString(R.string.dictionary_import_error_file_too_long, SettingsStore.CUSTOM_WORDS_IMPORT_MAX_LINES)); + return false; + } + + + private CustomWord createCustomWord(String line, int lineCount) { + try { + return new CustomWord( + CustomWordFile.getWord(line), + CustomWordFile.getLanguage(context, line) + ); + } catch (Exception e) { + String linePreview = line.length() > 50 ? line.substring(0, 50) + "..." : line; + sendFailure(resources.getString(R.string.dictionary_import_error_malformed_line, linePreview, lineCount)); + return null; + } + } +} 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/customWords/DictionaryExporter.java similarity index 96% rename from app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java rename to app/src/main/java/io/github/sspanak/tt9/db/customWords/DictionaryExporter.java index 35204a57..caff42c3 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/exporter/DictionaryExporter.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/DictionaryExporter.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db.exporter; +package io.github.sspanak.tt9.db.customWords; import android.app.Activity; import android.database.sqlite.SQLiteDatabase; @@ -31,7 +31,7 @@ public class DictionaryExporter extends AbstractExporter { } @Override - protected void exportSync(Activity activity) { + protected void runSync(Activity activity) { if (languages == null || languages.isEmpty()) { Logger.d(LOG_TAG, "Nothing to do"); return; diff --git a/app/src/main/java/io/github/sspanak/tt9/db/exporter/LogcatExporter.java b/app/src/main/java/io/github/sspanak/tt9/db/customWords/LogcatExporter.java similarity index 95% rename from app/src/main/java/io/github/sspanak/tt9/db/exporter/LogcatExporter.java rename to app/src/main/java/io/github/sspanak/tt9/db/customWords/LogcatExporter.java index 143d7b7b..99e95cb7 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/exporter/LogcatExporter.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/customWords/LogcatExporter.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db.exporter; +package io.github.sspanak.tt9.db.customWords; import android.app.Activity; @@ -60,7 +60,7 @@ public class LogcatExporter extends AbstractExporter { @Override - protected void exportSync(Activity activity) { + protected void runSync(Activity activity) { try { sendStart("Exporting logs..."); write(activity); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/AddWordResult.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/AddWordResult.java index 8774e3c5..1dbd00be 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/entities/AddWordResult.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/AddWordResult.java @@ -5,7 +5,6 @@ import android.content.Context; import androidx.annotation.NonNull; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.languages.Language; public class AddWordResult { public static final int CODE_SUCCESS = 0; diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWord.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWord.java new file mode 100644 index 00000000..88c2fbc2 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWord.java @@ -0,0 +1,26 @@ +package io.github.sspanak.tt9.db.entities; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.languages.NaturalLanguage; +import io.github.sspanak.tt9.languages.exceptions.InvalidLanguageCharactersException; + +public class CustomWord { + public final NaturalLanguage language; + public final String word; + public final String sequence; + + public CustomWord(@NonNull String word, NaturalLanguage language) throws InvalidLanguageCharactersException, IllegalArgumentException { + if (word.isEmpty() || language == null) { + throw new IllegalArgumentException("Word and language must be provided."); + } + + this.word = word; + this.language = language; + this.sequence = language.getDigitSequenceForWord(word); + + if (sequence.contains("1") || sequence.contains("0")) { + throw new IllegalArgumentException("Custom word: '" + word + "' contains punctuation."); + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWordFile.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWordFile.java new file mode 100644 index 00000000..8f9a2508 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/CustomWordFile.java @@ -0,0 +1,89 @@ +package io.github.sspanak.tt9.db.entities; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.languages.NaturalLanguage; +import io.github.sspanak.tt9.util.Logger; + +public class CustomWordFile { + public static final String MIME_TYPE = "text/*"; // for some reason, text/csv does not work as a filter when browsing + + private final ContentResolver contentResolver; + private final Uri fileUri; + private int size = -1; + + public CustomWordFile(Uri fileUri, @NonNull ContentResolver contentResolver) { + this.contentResolver = contentResolver; + this.fileUri = fileUri; + } + + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(contentResolver.openInputStream(fileUri))); + } + + public String getName() { + return fileUri.getLastPathSegment(); + } + + public boolean exists() { + try { + return getReader() != null; + } catch (IOException e) { + return false; + } + } + + public int getSize() { + if (size < 0) { + calculateSize(); + } + + return size; + } + + + private void calculateSize() { + size = 0; + + try { + BufferedReader reader = getReader(); + while (reader.readLine() != null) { + size++; + } + } catch (IOException e) { + Logger.w(getClass().getSimpleName(), "Failed to read file size. " + e.getMessage()); + } + } + + + public static NaturalLanguage getLanguage(@NonNull Context context, String line) { + if (line == null) { + return null; + } + + String[] parts = WordFile.splitLine(line); + if (parts == null || parts.length < 2) { + return null; + } + + try { + return LanguageCollection.getLanguage(context, Integer.parseInt(parts[1])); + } catch (NumberFormatException e) { + return null; + } + } + + @NonNull public static String getWord(String line) { + String[] parts = WordFile.splitLine(line); + return parts != null && parts.length > 0 ? parts[0] : ""; + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java index db5a0308..e98c4c7d 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java @@ -11,7 +11,6 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; -import io.github.sspanak.tt9.BuildConfig; import io.github.sspanak.tt9.R; import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.Logger; 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 59559286..e82eb5ab 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 @@ -66,6 +66,11 @@ public class ReadOps { } + public long countCustomWords(@NonNull SQLiteDatabase db) { + return CompiledQueryCache.simpleQueryForLong(db, "SELECT COUNT(*) FROM " + Tables.CUSTOM_WORDS, 0); + } + + public ArrayList getCustomWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String wordFilter) { ArrayList words = new ArrayList<>(); 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 index 4f8985df..a8baad83 100644 --- 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 @@ -3,88 +3,30 @@ package io.github.sspanak.tt9.preferences.items; import androidx.preference.Preference; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.exporter.AbstractExporter; import io.github.sspanak.tt9.preferences.PreferencesActivity; -import io.github.sspanak.tt9.ui.notifications.DictionaryProgressNotification; - -abstract public class ItemExportAbstract extends ItemClickable { - final protected PreferencesActivity activity; - final private Runnable onStart; - final private Runnable onFinish; +abstract public class ItemExportAbstract extends ItemProcessCustomWordsAbstract { public ItemExportAbstract(Preference item, PreferencesActivity activity, Runnable onStart, Runnable onFinish) { - super(item); - this.activity = activity; - this.onStart = onStart; - this.onFinish = onFinish; - - AbstractExporter exporter = getExporter(); - exporter.setFailureHandler(() -> onFinishExporting(null)); - exporter.setStartHandler(() -> activity.runOnUiThread(this::setLoadingStatus)); - exporter.setSuccessHandler(this::onFinishExporting); - refreshStatus(); + super(item, activity, onStart, onFinish); } - 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; + protected String getFailureTitle() { + return activity.getString(R.string.dictionary_export_failed); } - - abstract protected boolean onStartExporting(); - - - protected void onFinishExporting(String outputFile) { - activity.runOnUiThread(() -> { - setReadyStatus(); - - if (outputFile == null) { - DictionaryProgressNotification.getInstance(activity).showError( - activity.getString(R.string.dictionary_export_failed), - activity.getString(R.string.dictionary_export_failed_more_info) - ); - } else { - DictionaryProgressNotification.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) - ); - } - }); + @Override + protected String getFailureMessage() { + return activity.getString(R.string.dictionary_export_failed_more_info); } - - protected void setLoadingStatus() { - if (onStart != null) onStart.run(); - disable(); - - String loadingMessage = getExporter().getStatusMessage(); - item.setSummary(loadingMessage); - DictionaryProgressNotification.getInstance(activity).showLoadingMessage(loadingMessage, ""); + @Override + protected String getSuccessTitle() { + return activity.getString(R.string.dictionary_export_finished); } - - public void setReadyStatus() { - enable(); - if (onFinish != null) onFinish.run(); + @Override + protected String getSuccessMessage(String fileName) { + return activity.getString(R.string.dictionary_export_finished_more_info, fileName); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemProcessCustomWordsAbstract.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemProcessCustomWordsAbstract.java new file mode 100644 index 00000000..7a2d2953 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemProcessCustomWordsAbstract.java @@ -0,0 +1,97 @@ +package io.github.sspanak.tt9.preferences.items; + +import androidx.preference.Preference; + +import io.github.sspanak.tt9.db.customWords.AbstractFileProcessor; +import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.ui.notifications.DictionaryProgressNotification; + +abstract public class ItemProcessCustomWordsAbstract extends ItemClickable { + final protected PreferencesActivity activity; + final private Runnable onStart; + final private Runnable onFinish; + + + public ItemProcessCustomWordsAbstract(Preference item, PreferencesActivity activity, Runnable onStart, Runnable onFinish) { + super(item); + this.activity = activity; + this.onStart = onStart; + this.onFinish = onFinish; + + AbstractFileProcessor processor = getProcessor(); + processor.setFailureHandler(() -> onFinishProcessing(null)); + processor.setStartHandler(() -> activity.runOnUiThread(this::setLoadingStatus)); + processor.setSuccessHandler(this::onFinishProcessing); + refreshStatus(); + } + + + abstract protected AbstractFileProcessor getProcessor(); + + + public ItemProcessCustomWordsAbstract refreshStatus() { + if (item != null) { + if (getProcessor().isRunning()) { + setLoadingStatus(); + } else { + setReadyStatus(); + } + } + return this; + } + + + @Override + protected boolean onClick(Preference p) { + setLoadingStatus(); + if (!onStartProcessing()) { + setReadyStatus(); + } + return true; + } + + + abstract protected boolean onStartProcessing(); + + + protected void onFinishProcessing(String fileName) { + activity.runOnUiThread(() -> { + setReadyStatus(); + + if (fileName == null) { + DictionaryProgressNotification.getInstance(activity).showError( + getFailureTitle(), + getFailureMessage() + ); + } else { + DictionaryProgressNotification.getInstance(activity).showMessage( + getSuccessTitle(), + getSuccessMessage(fileName), + getSuccessMessage(fileName) + ); + } + }); + } + + + abstract protected String getFailureMessage(); + abstract protected String getFailureTitle(); + abstract protected String getSuccessMessage(String fileName); + abstract protected String getSuccessTitle(); + + + protected void setLoadingStatus() { + if (onStart != null) onStart.run(); + disable(); + + String loadingMessage = getProcessor().getStatusMessage(); + item.setSummary(loadingMessage); + DictionaryProgressNotification.getInstance(activity).showLoadingMessage(loadingMessage, ""); + } + + + public void setReadyStatus() { + enable(); + if (onFinish != null) onFinish.run(); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/DebugScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/DebugScreen.java index 4ae7addb..566a8bb4 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/DebugScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/DebugScreen.java @@ -3,7 +3,7 @@ package io.github.sspanak.tt9.preferences.screens.debug; import androidx.preference.SwitchPreferenceCompat; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.exporter.LogcatExporter; +import io.github.sspanak.tt9.db.customWords.LogcatExporter; import io.github.sspanak.tt9.hacks.DeviceInfo; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemText; diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/ItemExportLogcat.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/ItemExportLogcat.java index 3aaa9e8a..825d082a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/ItemExportLogcat.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/debug/ItemExportLogcat.java @@ -2,7 +2,7 @@ package io.github.sspanak.tt9.preferences.screens.debug; import androidx.preference.Preference; -import io.github.sspanak.tt9.db.exporter.LogcatExporter; +import io.github.sspanak.tt9.db.customWords.LogcatExporter; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemExportAbstract; import io.github.sspanak.tt9.ui.notifications.DictionaryProgressNotification; @@ -15,17 +15,17 @@ public class ItemExportLogcat extends ItemExportAbstract { } @Override - protected LogcatExporter getExporter() { + protected LogcatExporter getProcessor() { return LogcatExporter.getInstance(); } @Override - protected boolean onStartExporting() { - return getExporter().setIncludeSystemLogs(activity.getSettings().getEnableSystemLogs()).export(activity); + protected boolean onStartProcessing() { + return getProcessor().setIncludeSystemLogs(activity.getSettings().getEnableSystemLogs()).run(activity); } @Override - protected void onFinishExporting(String outputFile) { + protected void onFinishProcessing(String outputFile) { activity.runOnUiThread(() -> { DictionaryProgressNotification.getInstance(activity).hide(); setReadyStatus(); diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportCustomWords.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportCustomWords.java index 213d270f..8cc49ead 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportCustomWords.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportCustomWords.java @@ -3,7 +3,7 @@ package io.github.sspanak.tt9.preferences.screens.languages; import androidx.preference.Preference; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.exporter.CustomWordsExporter; +import io.github.sspanak.tt9.db.customWords.CustomWordsExporter; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemExportAbstract; @@ -15,12 +15,12 @@ class ItemExportCustomWords extends ItemExportAbstract { } @Override - protected CustomWordsExporter getExporter() { + protected CustomWordsExporter getProcessor() { return CustomWordsExporter.getInstance(); } - protected boolean onStartExporting() { - return CustomWordsExporter.getInstance().export(activity); + protected boolean onStartProcessing() { + return CustomWordsExporter.getInstance().run(activity); } public void setReadyStatus() { diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportDictionary.java index 5dd98486..1f3f1320 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportDictionary.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemExportDictionary.java @@ -2,10 +2,11 @@ package io.github.sspanak.tt9.preferences.screens.languages; import androidx.preference.Preference; -import io.github.sspanak.tt9.db.exporter.DictionaryExporter; +import io.github.sspanak.tt9.db.customWords.DictionaryExporter; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemExportAbstract; +import io.github.sspanak.tt9.preferences.items.ItemProcessCustomWordsAbstract; import io.github.sspanak.tt9.util.Logger; class ItemExportDictionary extends ItemExportAbstract { @@ -16,7 +17,7 @@ class ItemExportDictionary extends ItemExportAbstract { } @Override - public ItemExportAbstract refreshStatus() { + public ItemProcessCustomWordsAbstract refreshStatus() { if (item != null) { item.setVisible(Logger.isDebugLevel()); } @@ -24,14 +25,14 @@ class ItemExportDictionary extends ItemExportAbstract { } @Override - protected DictionaryExporter getExporter() { + protected DictionaryExporter getProcessor() { return DictionaryExporter.getInstance(); } - protected boolean onStartExporting() { + protected boolean onStartProcessing() { return DictionaryExporter.getInstance() .setLanguages(LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds())) - .export(activity); + .run(activity); } public void setReadyStatus() { diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemImportCustomWords.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemImportCustomWords.java new file mode 100644 index 00000000..b41d64ae --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemImportCustomWords.java @@ -0,0 +1,130 @@ +package io.github.sspanak.tt9.preferences.screens.languages; + +import android.app.Activity; +import android.content.Intent; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.preference.Preference; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.customWords.CustomWordsImporter; +import io.github.sspanak.tt9.db.entities.CustomWordFile; +import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.preferences.items.ItemProcessCustomWordsAbstract; +import io.github.sspanak.tt9.ui.notifications.DictionaryProgressNotification; +import io.github.sspanak.tt9.util.Logger; + +public class ItemImportCustomWords extends ItemProcessCustomWordsAbstract { + final public static String NAME = "dictionary_import_custom"; + + private ActivityResultLauncher importCustomWordsLauncher; + private CustomWordsImporter importer; + + private String lastError; + + public ItemImportCustomWords(Preference item, PreferencesActivity activity, Runnable onStart, Runnable onFinish) { + super(item, activity, onStart, onFinish); + } + + @Override + protected CustomWordsImporter getProcessor() { + if (importer == null) { + importer = CustomWordsImporter.getInstance(activity); + importer.setFailureHandler(this::onFailure); + importer.setProgressHandler(this::onProgress); + } + return importer; + } + + @Override + protected boolean onClick(Preference p) { + browseFiles(); + return true; + } + + @Override + protected boolean onStartProcessing() { + lastError = ""; + return false; + } + + private void onProgress(int progress) { + String loadingMsg = activity.getString(R.string.dictionary_import_progress, progress + "%"); + + DictionaryProgressNotification.getInstance(activity).showLoadingMessage(loadingMsg, "", progress, 100); + activity.runOnUiThread(() -> item.setSummary(loadingMsg)); + } + + private void onFailure(String error) { + lastError = error; + onFinishProcessing(null); + } + + @Override + protected String getFailureMessage() { + return lastError; + } + + @Override + protected String getFailureTitle() { + return activity.getString(R.string.dictionary_import_failed); + } + + @Override + protected String getSuccessMessage(String fileName) { + return ""; + } + + @Override + protected String getSuccessTitle() { + return activity.getString(R.string.dictionary_import_finished); + } + + @Override + public void setReadyStatus() { + item.setSummary(R.string.dictionary_import_custom_words_summary); + super.setReadyStatus(); + } + + void setBrowseFilesLauncher(ActivityResultLauncher launcher) { + if (item != null) { + item.setEnabled(true); + } + importCustomWordsLauncher = launcher; + } + + private void browseFiles() { + if (importCustomWordsLauncher == null) { + Logger.w(getClass().getSimpleName(), "No file browser launcher set"); + return; + } + + Intent intent = new Intent() + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CustomWordFile.MIME_TYPE) + .setAction(Intent.ACTION_GET_CONTENT); + + importCustomWordsLauncher.launch(intent); + } + + void onFileSelected(ActivityResult result) { + if (result.getResultCode() != Activity.RESULT_OK) { + onFailure(activity.getString(R.string.dictionary_import_error_browsing_error)); + Logger.e(getClass().getSimpleName(), "File picker activity failed with code: " + result.getResultCode()); + return; + } + + CustomWordFile file = new CustomWordFile( + result.getData() != null ? result.getData().getData() : null, + activity.getContentResolver() + ); + + if (!file.exists()) { + onFailure(activity.getString(R.string.dictionary_import_error_cannot_read_file)); + return; + } + + getProcessor().run(activity, file); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java index 3ff297fb..0a6049a1 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java @@ -1,11 +1,17 @@ package io.github.sspanak.tt9.preferences.screens.languages; +import android.content.Intent; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; + 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.db.customWords.CustomWordsExporter; +import io.github.sspanak.tt9.db.customWords.CustomWordsImporter; +import io.github.sspanak.tt9.db.customWords.DictionaryExporter; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemClickable; import io.github.sspanak.tt9.preferences.screens.BaseScreenFragment; @@ -16,6 +22,7 @@ public class LanguagesScreen extends BaseScreenFragment { private final ArrayList clickables = new ArrayList<>(); private ItemLoadDictionary loadItem; + private ItemImportCustomWords importCustomWordsItem; private ItemExportDictionary exportDictionaryItem; private ItemExportCustomWords exportCustomWordsItem; @@ -39,13 +46,15 @@ public class LanguagesScreen extends BaseScreenFragment { .populate() .enableClickHandler(); - loadItem = new ItemLoadDictionary(findPreference(ItemLoadDictionary.NAME), + loadItem = new ItemLoadDictionary( + findPreference(ItemLoadDictionary.NAME), activity, () -> ItemClickable.disableOthers(clickables, loadItem), this::onActionFinish ); - exportDictionaryItem = new ItemExportDictionary(findPreference(ItemExportDictionary.NAME), + exportDictionaryItem = new ItemExportDictionary( + findPreference(ItemExportDictionary.NAME), activity, this::onActionStart, this::onActionFinish @@ -74,14 +83,23 @@ public class LanguagesScreen extends BaseScreenFragment { findPreference(ItemExportCustomWords.NAME), activity, this::onActionStart, - this::onActionFinish); - + this::onActionFinish + ); clickables.add(exportCustomWordsItem); + importCustomWordsItem = new ItemImportCustomWords( + findPreference(ItemImportCustomWords.NAME), + activity, + this::onActionStart, + this::onActionFinish + ); + clickables.add(importCustomWordsItem); + ItemClickable.enableAllClickHandlers(clickables); refreshItems(); resetFontSize(false); + createBrowseFilesLauncher(); } @@ -96,11 +114,16 @@ public class LanguagesScreen extends BaseScreenFragment { loadItem.refreshStatus(); exportDictionaryItem.refreshStatus(); exportCustomWordsItem.refreshStatus(); + importCustomWordsItem.refreshStatus(); if (DictionaryLoader.getInstance(activity).isRunning()) { loadItem.refreshStatus(); ItemClickable.disableOthers(clickables, loadItem); - } else if (CustomWordsExporter.getInstance().isRunning() || DictionaryExporter.getInstance().isRunning()) { + } else if ( + CustomWordsExporter.getInstance().isRunning() + || DictionaryExporter.getInstance().isRunning() + || CustomWordsImporter.getInstance(activity).isRunning() + ) { onActionStart(); } else { onActionFinish(); @@ -115,4 +138,13 @@ public class LanguagesScreen extends BaseScreenFragment { private void onActionFinish() { ItemClickable.enableAll(clickables); } + + private void createBrowseFilesLauncher() { + ActivityResultLauncher launcher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> importCustomWordsItem.onFileSelected(result) + ); + + importCustomWordsItem.setBrowseFilesLauncher(launcher); + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java index e637df6e..08876512 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java @@ -9,6 +9,8 @@ public class SettingsStore extends SettingsUI { /************* internal settings *************/ public final static int CLIPBOARD_PREVIEW_LENGTH = 20; public final static int DELETE_WORDS_SEARCH_DELAY = 500; // ms + public final static int CUSTOM_WORDS_IMPORT_MAX_LINES = 250; + public final static int CUSTOM_WORDS_MAX = 1000; public final static int DICTIONARY_AUTO_LOAD_COOLDOWN_TIME = 1200000; // 20 minutes in ms public final static int DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT = 10000; // ms public final static int DICTIONARY_DOWNLOAD_READ_TIMEOUT = 10000; // ms diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryProgressNotification.java b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryProgressNotification.java index e2ffc2e8..661d9374 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryProgressNotification.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryProgressNotification.java @@ -50,12 +50,17 @@ public class DictionaryProgressNotification extends DictionaryNotification { public void showLoadingMessage(@NonNull String title, @NonNull String message) { + showLoadingMessage(title, message, 0, 1); + } + + + public void showLoadingMessage(@NonNull String title, @NonNull String message, int progress, int maxProgress) { this.title = title; this.message = message; messageLong = ""; - indeterminate = true; - progress = 1; - maxProgress = 2; + this.progress = progress; + this.maxProgress = maxProgress; + indeterminate = (progress <= 0 && maxProgress <= 0); renderMessage(); } diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index b80edc5f..d448d597 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -54,6 +54,13 @@ Експортиране на CSV с всички добавени думи в: „%1$s“. Неуспешно експортиране За повече информация, активирайте режима за отстраняване на грешки и прегледайте журнала. + Неуспешно импортиране. + Импортирането завърши. + Грешка при избора на файлове в Android. + Избраният файл не може да бъде отворен. + Думите надвишават максимално допустимия брой от %1$d. + Неочакван формат на дума: „%1$s“ на ред %2$d. + Хранилището за добавени думи е пълно. Не можете да импортирате повече думи. Изтрий Намери и изтрий на неправилно написани или ненужни думи. Търси думи @@ -154,4 +161,8 @@ Избор на клавиатура Гласово въвеждане Добавяне без потвърждение + Импортиране + Импортиране на думи от по-рано експортирано CSV. + Импортиране на CSV (%1$s)… + Импортиране на CSV… diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3aceabac..896d4163 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -92,6 +92,13 @@ 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. + Importieren fehlgeschlagen. + Importieren abgeschlossen. + Fehler beim Dateiauswahl von Android. + Die ausgewählte Datei konnte nicht geöffnet werden. + Die Anzahl der Wörter überschreitet die maximal erlaubte Anzahl von %1$d. + Unerwartetes Wortformat: \"%1$s\" in Zeile %2$d. + Der Speicher für hinzugefügte Wörter ist voll. Sie können keine weiteren Wörter importieren. Löschen Finde und lösche falsch geschriebene oder überflüssige Wörter. Nach Wörtern suchen @@ -143,4 +150,8 @@ Rechts Tastaturauswahl Ohne Bestätigung hinzufügen + Importieren + Wörter aus einer zuvor exportierten CSV-Datei importieren. + CSV importieren (%1$s)… + CSV importieren… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7a4301d3..2cb35729 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -105,6 +105,13 @@ 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. + La importación falló. + La importación se completó. + Error del selector de archivos de Android. + No se pudo abrir el archivo seleccionado. + El número de palabras excede el límite máximo permitido de %1$d. + Formato de palabra inesperado: \"%1$s\" en la línea %2$d. + El almacenamiento de palabras añadidas está lleno. No puede importar más palabras. Eliminar Buscar y eliminar palabras mal escritas o innecesarias. Buscar palabras @@ -152,4 +159,8 @@ A la derecha Cambiar el teclado Añadir sin confirmación + Importar + Importar palabras de un CSV previamente exportado. + Importando CSV (%1$s)… + Importando CSV… diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e1b2f64b..17d80d94 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -107,6 +107,13 @@ 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. + L\'importation a échoué. + Importation terminée. + Erreur du sélecteur de fichiers d\'Android. + Impossible d\'ouvrir le fichier sélectionné. + Le nombre de mots dépasse le nombre maximal autorisé de %1$d. + Format de mot inattendu : « %1$s » à la ligne %2$d. + Le stockage des mots ajoutés est plein. Vous ne pouvez plus importer de mots. Supprimer Trouver et supprimer des mots mal orthographiés ou inutiles. Rechercher des mots @@ -150,4 +157,8 @@ À droite Choisir le clavier Ajouter sans confirmation + Importer + Importer des mots à partir d\'un fichier CSV précédemment exporté. + Importation de CSV (%1$s)… + Importation de CSV… diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2e494304..df6a68fd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -92,6 +92,13 @@ 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. + Importazione fallita. + Importazione completata. + Errore del selettore di file di Android. + Impossibile aprire il file selezionato. + Le parole superano il numero massimo consentito di %1$d. + Formato di parola imprevisto: \"%1$s\" alla linea %2$d. + Lo spazio di archiviazione delle parole aggiunte è pieno. Non è possibile importare altre parole. Elimina Trova ed elimina parole errate o non necessarie. Ricerca di parole @@ -142,5 +149,9 @@ A sinistra A destra Aggiungere senza conferma + Importare + Importare parole da un CSV precedentemente esportato. + Importazione CSV (%1$s)… + Importazione CSV… diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 1ba5d674..76a6c665 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -105,6 +105,13 @@ ייצוא CSV עם כל המילים שנוספו ל: \"%1$s\". נכשל בייצוא "למידע נוסף, הפעל מצב איתור באגים וראה את הלוגים. " + הייבוא נכשל. + הייבוא הושלם. + שגיאת בורר הקבצים של Android. + לא ניתן לפתוח את הקובץ שנבחר. + מספר המילים חורג מהמכסה המקסימלית של %1$d. + תבנית מילה בלתי צפויה: \"%1$s\" בשורה %2$d. + אחסון המילים שהתווספו מלא. אינך יכול לייבא עוד מילים. מחיקה מצא ומחק מילים שכתובות בטעות או שאינן נדרשות. חיפוש מילים @@ -155,4 +162,8 @@ ימינה בחירת מקלדת להוסיף ללא אישור + לְיַבֵּא + ייבוא מילים מקובץ CSV שיוצא קודם לכן. + מייבא CSV (%1$s)… + מייבא CSV… diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 752f5634..0e77194b 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -124,6 +124,13 @@ Žodžiai eksportuoti į: „%1$s“. Eksportuojama CSV… Eksportuojama CSV (%1$s)… + Importuoti nepavyko. + Importavimas baigtas. + Android failų pasirinkimo klaida. + Nepavyko atidaryti pasirinkto failo. + Žodžių skaičius viršija leistiną maksimumą %1$d. + Nenumatytas žodžio formatas: „%1$s“ eilutėje %2$d. + Pridėtų žodžių saugykla yra pilna. Daugiau žodžių importuoti negalite. Ištrinti Raskite ir ištrinkite neteisingai parašytus arba nereikalingus žodžius. Ieškoti žodžių @@ -161,4 +168,8 @@ Dešinėje Keisti klaviatūrą Pridėti be patvirtinimo + Importuoti + Importuoti žodžius iš anksčiau eksportuoto CSV. + Importuojamas CSV (%1$s)… + Importuojamas CSV… diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index da1b249a..70eefe76 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -90,6 +90,13 @@ Exporteer een CSV met alle toegevoegde woorden naar: \"%1$s\". Exporteren mislukt Voor meer informatie, schakel de debug-modus in en bekijk de logs. + Importeren mislukt. + Importeren voltooid. + Fout bij het selecteren van bestanden in Android. + Kan het geselecteerde bestand niet openen. + Woorden overschrijden het maximaal toegestane aantal van %1$d. + Onverwacht woordformaat: \"%1$s\" op regel %2$d. + De opslag voor toegevoegde woorden is vol. U kunt geen woorden meer importeren. Verwijderen Zoek en verwijder verkeerd gespelde of onnodige woorden. Zoeken naar woorden @@ -141,4 +148,8 @@ Rechts Toetsenbordkeuze Toevoegen zonder bevestiging + Importeren + Woorden importeren uit een eerder geëxporteerde CSV. + CSV importeren (%1$s)… + CSV importeren… diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 81d75cd0..b1f00e39 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -105,6 +105,13 @@ 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. + Importação falhou. + Importação concluída. + Erro no seletor de arquivos do Android. + Não foi possível abrir o arquivo selecionado. + O número de palavras excede o máximo permitido de %1$d. + Formato de palavra inesperado: \"%1$s\" na linha %2$d. + O armazenamento de palavras adicionadas está cheio. Você não pode importar mais palavras. Excluir Encontrar e excluir palavras escritas incorretamente ou desnecessárias. Buscar palavras @@ -155,4 +162,8 @@ À direita Mude o teclado Adicionar sem confirmação + Importar + Importar palavras de um CSV previamente exportado. + Importando CSV (%1$s)… + Importando CSV… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ef3f98cd..d2775252 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -106,6 +106,13 @@ Экспорт CSV со всеми добавленными словами в: «%1$s». Ошибка экспорта Для получения дополнительной информации включите режим отладки и просмотрите журналы. + Импорт не выполнен. + Импорт завершен. + Ошибка выбора файлов в Android. + Не удалось открыть выбранный файл. + Количество слов превышает максимально допустимое значение %1$d. + Неожиданный формат слова: «%1$s» на строке %2$d. + Хранилище добавленных слов заполнено. Вы не можете импортировать больше слов. Удалить Найти и удалить ошибочно написанные или ненужные слова. Поиск слов @@ -152,4 +159,8 @@ Направо Выбор клавиатуры Добавить без подтверждения + Импортировать + Импортировать слова из ранее экспортированного CSV. + Импортирование CSV (%1$s)… + Импортирование CSV… diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 53374418..bfe887d9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -90,6 +90,13 @@ Seçilenleri CSV dosyası olarak „%1$s“ dizinine dışa aktar. Dışa aktarım başarısız oldu. Daha fazla bilgi için Hata Ayıklamayı açıp kayıtları kontrol edin. + İçe aktarma başarısız oldu. + İçe aktarma tamamlandı. + Android dosya seçici hatası. + Seçilen dosya açılamadı. + Kelime sayısı izin verilen maksimum %1$d\'i aşıyor. + Beklenmeyen kelime formatı: \"%1$s\" satır %2$d\'de. + Eklenen kelime depolama alanı dolu. Daha fazla kelime içe aktaramazsınız. Sil Yanlış yazılan ya da gereksiz kelimeleri bulun ve silin. Aramak istediğiniz kelimeyi yazın… @@ -155,4 +162,8 @@ Sağa Klavye Seçimi Onay olmadan ekle + İçe Aktar + Daha önce dışa aktarılan bir CSV\'den kelimeleri içe aktar. + CSV İçe aktarılıyor (%1$s)… + CSV İçe aktarılıyor… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 254a6864..c94624c6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -89,6 +89,13 @@ Експорт CSV з усіма доданими словами в: \"%1$s\". Помилка експорту Для отримання додаткової інформації увімкніть режим відлагодження та перегляньте журнали. + Імпорт не вдався. + Імпорт завершено. + Помилка вибору файлів Android. + Не вдалося відкрити вибраний файл. + Кількість слів перевищує максимально дозволену кількість %1$d. + "Несподіваний формат слова: \"%1$s\" у рядку %2$d. " + Сховище доданих слів заповнено. Ви не можете імпортувати більше слів. Видалити Знайти та видалити неправильно написані або зайві слова. Пошук слів @@ -163,4 +170,8 @@ Праворуч Змінити клавіатуру Додати без підтвердження + Імпортувати + Імпортувати слова з раніше експортованого CSV. + Імпорт CSV (%1$s)… + Імпорт CSV… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 65ef51d2..4398e4f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,18 @@ Exporting CSV… Exporting CSV (%1$s)… + Import + Import words from a previously exported CSV. + Importing failed. + Importing completed. + Android file picker error. + Could not open the selected file. + Words exceed the maximum allowed count of %1$d. + Unexpected word format: \"%1$s\" on line %2$d. + The added word storage is full. You can not import any more words. + Importing CSV (%1$s)… + Importing CSV… + Delete Find and delete misspelled or unneeded words. Search for Words diff --git a/app/src/main/res/xml/prefs_screen_languages.xml b/app/src/main/res/xml/prefs_screen_languages.xml index fd963b39..9c70af92 100644 --- a/app/src/main/res/xml/prefs_screen_languages.xml +++ b/app/src/main/res/xml/prefs_screen_languages.xml @@ -36,6 +36,11 @@ app:key="add_word_no_confirmation" app:title="@string/add_word_no_confirmation" /> + +