'full' and 'lite' flavors
This commit is contained in:
parent
11e042e707
commit
6670bccc50
16 changed files with 182 additions and 64 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -27,8 +27,8 @@ jobs:
|
|||
- name: Validate Dictionaries
|
||||
run: ./gradlew validateLanguages
|
||||
- name: Build Languages
|
||||
run: ./gradlew copyLanguages writeDictionaryProperties
|
||||
run: ./gradlew copyDefinitions copyDictionaries writeDictionaryProperties
|
||||
- name: Lint
|
||||
run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks
|
||||
- name: Build Release APK
|
||||
- name: Build all APK variants
|
||||
run: ./gradlew build
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -16,6 +16,8 @@ gen/
|
|||
.gradle/
|
||||
assets/
|
||||
build/
|
||||
full/
|
||||
lite/
|
||||
release/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ Thanks to your donations, a brand new testing device is available, a Sonim XP380
|
|||
## 💪 Privacy Policy and Philosophy
|
||||
- No ads, no premium or paid features. It's all free.
|
||||
- No spying, no tracking, no telemetry or reports. No nothing!
|
||||
- No network connectivity, except when voice input is active.
|
||||
- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission.
|
||||
- It only does its job.
|
||||
- Open-source, so you can verify all the above yourself.
|
||||
- Created with help from the entire community.
|
||||
|
|
|
|||
|
|
@ -17,22 +17,25 @@ tasks.register('validateLanguages') {
|
|||
}
|
||||
}
|
||||
|
||||
tasks.register('copyLanguages', Copy) {
|
||||
tasks.register('copyDefinitions', Copy) {
|
||||
from LANGUAGES_INPUT_DIR
|
||||
include '**/*.csv'
|
||||
include '**/*.txt'
|
||||
include '**/*.yml'
|
||||
into LANGUAGES_OUTPUT_DIR
|
||||
}
|
||||
|
||||
tasks.register('copyDictionaries', Copy) {
|
||||
from DICTIONARIES_INPUT_DIR
|
||||
include '**/*.csv'
|
||||
include '**/*.txt'
|
||||
into DICTIONARIES_OUTPUT_DIR
|
||||
}
|
||||
|
||||
tasks.register('writeDictionaryProperties') {
|
||||
inputs.dir fileTree(dir: DICTIONARIES_INPUT_DIR)
|
||||
outputs.dir DICTIONARIES_OUTPUT_DIR
|
||||
outputs.dir DICTIONARY_META_OUTPUT_DIR
|
||||
|
||||
doLast {
|
||||
[getDictionarySizes, getDictionaryHashes].parallelStream().forEach { action ->
|
||||
action(DICTIONARIES_INPUT_DIR, DICTIONARIES_OUTPUT_DIR)
|
||||
}
|
||||
getDictionaryProperties(DICTIONARIES_INPUT_DIR, DICTIONARY_META_OUTPUT_DIR)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +47,7 @@ tasks.register('updateManifest') {
|
|||
|
||||
clean {
|
||||
delete LANGUAGES_OUTPUT_DIR
|
||||
delete DICTIONARIES_OUTPUT_DIR
|
||||
}
|
||||
|
||||
// using the exported Closures directly causes weird values, hence the extra wrappers here
|
||||
|
|
@ -87,24 +91,35 @@ android {
|
|||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
tasks.named("generate${variant.name.capitalize()}Assets")?.configure {
|
||||
dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties)
|
||||
}
|
||||
flavorDimensions = ['app']
|
||||
productFlavors {
|
||||
full { dimension 'app' }
|
||||
lite { dimension 'app' }
|
||||
}
|
||||
|
||||
["lintAnalyzeDebug", "generateDebugLintReportModel", "lintVitalAnalyzeRelease", "generateReleaseLintVitalReportModel"].each { taskName ->
|
||||
applicationVariants.configureEach { variant ->
|
||||
[
|
||||
"merge${variant.name.capitalize()}Assets",
|
||||
"lintAnalyze${variant.name.capitalize()}",
|
||||
"generate${variant.name.capitalize()}LintReportModel",
|
||||
"lintVitalAnalyze${variant.name.capitalize()}",
|
||||
"generate${variant.name.capitalize()}LintVitalReportModel"
|
||||
].each { taskName ->
|
||||
try {
|
||||
tasks.named(taskName)?.configure {
|
||||
dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties)
|
||||
dependsOn(validateLanguages, copyDefinitions, copyDictionaries, writeDictionaryProperties)
|
||||
}
|
||||
} catch (UnknownTaskException ignored) {}
|
||||
}
|
||||
|
||||
assembleDebug.finalizedBy(updateManifest)
|
||||
assembleRelease.finalizedBy(updateManifest)
|
||||
assembleLiteDebug.finalizedBy(updateManifest)
|
||||
assembleFullDebug.finalizedBy(updateManifest)
|
||||
assembleLiteRelease.finalizedBy(updateManifest)
|
||||
assembleFullRelease.finalizedBy(updateManifest)
|
||||
|
||||
variant.outputs.configureEach {
|
||||
outputFileName = "${APP_NAME}-v${getVerName()}.apk"
|
||||
def suffix = variant.flavorName == 'full' ? '-full' : ''
|
||||
outputFileName = "${APP_NAME}-v${getVerName()}${suffix}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ ext.DICTIONARIES_DIR_NAME = 'dictionaries'
|
|||
ext.DICTIONARY_SIZES_DIR_NAME = 'dictionary-sizes'
|
||||
|
||||
def ROOT_DIR = "${project.rootDir}/app"
|
||||
def ASSETS_DIR = "${ROOT_DIR}/src/main/assets"
|
||||
def MAIN_ASSETS_DIR = "${ROOT_DIR}/src/main/assets"
|
||||
def FULL_VERSION_ASSETS_DIR = "${ROOT_DIR}/src/full/assets"
|
||||
|
||||
ext.LANGUAGES_INPUT_DIR = "${ROOT_DIR}/${LANGUAGES_DIR_NAME}"
|
||||
ext.DEFINITIONS_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DEFINITIONS_DIR_NAME}"
|
||||
ext.DICTIONARIES_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DICTIONARIES_DIR_NAME}"
|
||||
|
||||
ext.LANGUAGES_OUTPUT_DIR = "${ASSETS_DIR}/${LANGUAGES_DIR_NAME}"
|
||||
ext.LANGUAGES_OUTPUT_DIR = "${MAIN_ASSETS_DIR}/${LANGUAGES_DIR_NAME}"
|
||||
ext.DEFINITIONS_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DEFINITIONS_DIR_NAME}"
|
||||
ext.DICTIONARIES_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}"
|
||||
ext.DICTIONARY_META_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}"
|
||||
ext.DICTIONARIES_OUTPUT_DIR = "${FULL_VERSION_ASSETS_DIR}/${LANGUAGES_DIR_NAME}/${DICTIONARIES_DIR_NAME}"
|
||||
|
||||
ext.LANGUAGE_VALIDATION_DIR = layout.buildDirectory.dir("langValidation")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
ext.getDictionarySizes = { dictionariesDir, sizesDir ->
|
||||
fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary ->
|
||||
def dictionarySize = dictionary.exists() ? dictionary.text.split("\n").length : 0
|
||||
new File(sizesDir, "${dictionary.getName()}.size").text = dictionarySize
|
||||
}
|
||||
}
|
||||
|
||||
ext.getDictionaryHashes = { dictionariesDir, timestampsDir ->
|
||||
ext.getDictionaryProperties = { dictionariesDir, sizesDir ->
|
||||
fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary ->
|
||||
def hash = dictionary.exists() ? dictionary.text.digest("SHA-1") : ""
|
||||
new File(timestampsDir, "${dictionary.getName()}.hash").text = hash
|
||||
def revision = dictionary.exists() ? exec("git log --pretty=tformat:%H -n 1 ${dictionary}") : ""
|
||||
def words = dictionary.exists() ? dictionary.text.split("\n").length : 0
|
||||
|
||||
new File(sizesDir, "${dictionary.getName()}.props.yml").text = "hash: ${hash}\nrevision: ${revision}\nwords: ${words}"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
app/src/lite/AndroidManifest.xml
Normal file
3
app/src/lite/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/> <!-- allows dictionary download -->
|
||||
</manifest>
|
||||
|
|
@ -68,7 +68,7 @@ public class DictionaryLoader {
|
|||
}
|
||||
|
||||
|
||||
public boolean load(ArrayList<Language> languages) {
|
||||
public boolean load(Context context, ArrayList<Language> languages) {
|
||||
if (isRunning()) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ public class DictionaryLoader {
|
|||
if (isInterrupted()) {
|
||||
break;
|
||||
}
|
||||
importAll(lang);
|
||||
importAll(context, lang);
|
||||
currentFile++;
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ public class DictionaryLoader {
|
|||
public static void load(Context context, Language language) {
|
||||
DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context);
|
||||
getInstance(context).setOnStatusChange(status -> progressBar.show(context, status));
|
||||
self.load(new ArrayList<Language>() {{ add(language); }});
|
||||
self.load(context, new ArrayList<Language>() {{ add(language); }});
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ public class DictionaryLoader {
|
|||
load(context, language);
|
||||
}
|
||||
// or if the database is outdated, compared to the dictionary file, ask for confirmation and load
|
||||
else if (!hash.equals(new WordFile(language.getDictionaryFile(), self.assets).getHash())) {
|
||||
else if (!hash.equals(new WordFile(context, language.getDictionaryFile(), self.assets).getHash())) {
|
||||
new DictionaryUpdateNotification(context, language).show();
|
||||
}
|
||||
},
|
||||
|
|
@ -151,7 +151,7 @@ public class DictionaryLoader {
|
|||
}
|
||||
|
||||
|
||||
private void importAll(Language language) {
|
||||
private void importAll(Context context, Language language) {
|
||||
if (language == null) {
|
||||
Logger.e(LOG_TAG, "Failed loading a dictionary for NULL language.");
|
||||
sendError(InvalidLanguageException.class.getSimpleName(), -1);
|
||||
|
|
@ -178,7 +178,7 @@ public class DictionaryLoader {
|
|||
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||
logLoadingStep("Letters imported", language, Timer.restart());
|
||||
|
||||
importWordFile(language, lettersCount, progress, 88);
|
||||
importWordFile(context, language, lettersCount, progress, 88);
|
||||
progress = 88;
|
||||
sendProgressMessage(language, progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||
logLoadingStep("Dictionary file imported", language, Timer.restart());
|
||||
|
|
@ -252,8 +252,8 @@ public class DictionaryLoader {
|
|||
}
|
||||
|
||||
|
||||
private void importWordFile(Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
|
||||
WordFile wordFile = new WordFile(language.getDictionaryFile(), assets);
|
||||
private void importWordFile(Context context, Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
|
||||
WordFile wordFile = new WordFile(context, language.getDictionaryFile(), assets);
|
||||
WordBatch batch = new WordBatch(language, wordFile.getTotalLines());
|
||||
int currentLine = 1;
|
||||
float progressRatio = (maxProgress - minProgress) / wordFile.getTotalLines();
|
||||
|
|
|
|||
|
|
@ -1,27 +1,39 @@
|
|||
package io.github.sspanak.tt9.db.entities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
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;
|
||||
|
||||
public class WordFile {
|
||||
private static final String LOG_TAG = WordFile.class.getSimpleName();
|
||||
|
||||
private final AssetManager assets;
|
||||
private final Context context;
|
||||
private final String name;
|
||||
private String hash = null;
|
||||
private String downloadUrl = null;
|
||||
private int totalLines = -1;
|
||||
|
||||
public WordFile(String name, AssetManager assets) {
|
||||
|
||||
public WordFile(Context context, String name, AssetManager assets) {
|
||||
this.assets = assets;
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
public static String[] splitLine(String line) {
|
||||
String[] parts = { line, "" };
|
||||
|
||||
|
|
@ -38,6 +50,7 @@ public class WordFile {
|
|||
return parts;
|
||||
}
|
||||
|
||||
|
||||
public static short getFrequencyFromLineParts(String[] frequencyParts) {
|
||||
try {
|
||||
return Short.parseShort(frequencyParts[1]);
|
||||
|
|
@ -46,40 +59,115 @@ public class WordFile {
|
|||
}
|
||||
}
|
||||
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(assets.open(name), StandardCharsets.UTF_8));
|
||||
|
||||
public boolean exists() {
|
||||
try {
|
||||
assets.open(name).close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getTotalLines() {
|
||||
if (totalLines < 0) {
|
||||
String rawTotalLines = getProperty("size");
|
||||
try {
|
||||
totalLines = Integer.parseInt(rawTotalLines);
|
||||
} catch (Exception e) {
|
||||
Logger.w(LOG_TAG, "Invalid 'size' property of: " + name + ". Expecting an integer, got: '" + rawTotalLines + "'.");
|
||||
totalLines = 0;
|
||||
}
|
||||
|
||||
public InputStream getRemoteStream() throws IOException {
|
||||
URLConnection connection = new URL(getDownloadUrl()).openConnection();
|
||||
connection.setConnectTimeout(SettingsStore.DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT);
|
||||
connection.setReadTimeout(SettingsStore.DICTIONARY_DOWNLOAD_READ_TIMEOUT);
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
|
||||
public BufferedReader getReader() throws IOException {
|
||||
InputStream stream = exists() ? assets.open(name) : getRemoteStream();
|
||||
return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
private String getDownloadUrl() {
|
||||
if (downloadUrl == null) {
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
return totalLines;
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
|
||||
private void setDownloadUrl(String rawProperty, String rawValue) {
|
||||
if (!rawProperty.equals("revision")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String revision = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
|
||||
downloadUrl = revision.isEmpty() ? null : context.getString(R.string.dictionary_url, revision, name);
|
||||
|
||||
if (revision.isEmpty()) {
|
||||
Logger.w(LOG_TAG, "Invalid 'revision' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getHash() {
|
||||
if (hash == null) {
|
||||
hash = getProperty("hash");
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private String getProperty(String propertyName) {
|
||||
String propertyFilename = name + "." + propertyName;
|
||||
|
||||
private void setHash(String rawProperty, String rawValue) {
|
||||
if (!rawProperty.equals("hash")) {
|
||||
return;
|
||||
}
|
||||
|
||||
hash = rawValue == null || rawValue.isEmpty() ? "" : rawValue;
|
||||
|
||||
if (hash.isEmpty()) {
|
||||
Logger.w(LOG_TAG, "Invalid 'hash' property of: " + name + ". Expecting a string, got: '" + rawValue + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getTotalLines() {
|
||||
if (totalLines < 0) {
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
return totalLines;
|
||||
}
|
||||
|
||||
|
||||
private void setTotalLines(String rawProperty, String rawValue) {
|
||||
if (!rawProperty.equals("words")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
totalLines = Integer.parseInt(rawValue);
|
||||
} catch (Exception e) {
|
||||
Logger.w(LOG_TAG, "Invalid 'words' property of: " + name + ". Expecting an integer, got: '" + rawValue + "'.");
|
||||
totalLines = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void loadProperties() {
|
||||
String propertyFilename = name + ".props.yml";
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(propertyFilename)))) {
|
||||
return reader.readLine();
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
String[] parts = line.split("\\s*:\\s*");
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setDownloadUrl(parts[0], parts[1]);
|
||||
setHash(parts[0], parts[1]);
|
||||
setTotalLines(parts[0], parts[1]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.w(LOG_TAG, "Could not read the '" + propertyName + "' property of: " + name + " from: " + propertyFilename + ". " + e.getMessage());
|
||||
return "";
|
||||
Logger.w(LOG_TAG, "Could not read the property file: " + propertyFilename + ". " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ItemLoadDictionary extends ItemClickable {
|
|||
ArrayList<Language> languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds());
|
||||
|
||||
setLoadingStatus();
|
||||
if (!loader.load(languages)) {
|
||||
if (!loader.load(activity, languages)) {
|
||||
loader.stop();
|
||||
setReadyStatus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public class SettingsStore extends SettingsUI {
|
|||
/************* internal settings *************/
|
||||
public final static int DELETE_WORDS_SEARCH_DELAY = 500; // ms
|
||||
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
|
||||
public final static int DICTIONARY_IMPORT_BATCH_SIZE = 5000; // words
|
||||
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms
|
||||
public final static byte SLOW_QUERY_TIME = 50; // ms
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import android.os.Bundle;
|
|||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
|
||||
import io.github.sspanak.tt9.R;
|
||||
|
|
@ -138,6 +140,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification {
|
|||
message = resources.getString(R.string.add_word_invalid_language);
|
||||
} else if (errorType.equals(DictionaryImportException.class.getSimpleName()) || errorType.equals(InvalidLanguageCharactersException.class.getSimpleName())) {
|
||||
message = resources.getString(R.string.dictionary_load_bad_char, word, line, lang.getName());
|
||||
} else if (errorType.equals(UnknownHostException.class.getSimpleName()) || errorType.equals(SocketException.class.getSimpleName())) {
|
||||
message = resources.getString(R.string.dictionary_load_no_internet, lang.getName());
|
||||
} else if (errorType.equals(IOException.class.getSimpleName()) || errorType.equals(FileNotFoundException.class.getSimpleName())) {
|
||||
message = resources.getString(R.string.dictionary_not_found, lang.getName());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string translatable="false" name="dictionary_url">https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s</string>
|
||||
<string translatable="false" name="help_url">https://github.com/sspanak/tt9/blob/master/docs/user-manual.md</string>
|
||||
<string name="app_name" translatable="false">Traditional T9</string>
|
||||
<string name="app_name_short" translatable="false">TT9</string>
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
<string name="dictionary_cancel_load">Cancel Loading</string>
|
||||
<string name="dictionary_load_bad_char">Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\".</string>
|
||||
<string name="dictionary_load_error">Failed loading the dictionary for language \"%1$s\" (%2$s).</string>
|
||||
<string name="dictionary_load_no_internet">Failed downloading the dictionary for language \"%1$s\". Check your Internet connection.</string>
|
||||
<string name="dictionary_load_cancelled">Dictionary load cancelled.</string>
|
||||
<string name="dictionary_loaded">Dictionary load completed.</string>
|
||||
<string name="dictionary_loading">Loading dictionary (%1$s)…</string>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ def generateVersionName() {
|
|||
return "$versionTagsCount.$commitsSinceLastTag$betaString"
|
||||
}
|
||||
|
||||
ext.exec = { command ->
|
||||
return execThing(command)
|
||||
}
|
||||
|
||||
ext.getVersionName = { ->
|
||||
return generateVersionName()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текста на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
|
||||
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текст на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
|
||||
|
||||
Поддържани езици: арабски, български, хърватски, чешки, датски, холандски, английски, финландски, френски, немски, гръцки, иврит, унгарски, индонезийски, италиански, кисуахили, норвежки, полски, португалски (европейски и бразилски), румънски, руски, испански, шведски, турски, украински, идиш.
|
||||
|
||||
Философия и защита не личните данни:
|
||||
- Без реклами, специални или платени функции. Всичко е напълно безплатно.
|
||||
- Без шпиониране, следене, телеметрия и отчети. Без глупости!
|
||||
- Без връзка към интернет, освен когато е активно гласовото въвеждане.
|
||||
- Използва интернет само при активно гласово въвеждане и за изтегляне на речници от Github. Можете да изберете и пълната версия, която съдържа всички езици и не изисква разрешението за интернет.
|
||||
- Единствено си върши работата.
|
||||
- С отворен код, така че може да проверите горното и сами.
|
||||
- Създадена с помощта на цялата общност.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Supported languages: Arabic, Bulgarian, Croatian, Czech, Danish, Dutch, English,
|
|||
Privacy Policy and Philosophy:
|
||||
- No ads, no premium or paid features. It's all free.
|
||||
- No spying, no tracking, no telemetry or reports. No nothing!
|
||||
- No network connectivity, except when voice input is active.
|
||||
- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission.
|
||||
- It only does its job.
|
||||
- Open-source, so you can verify all the above yourself.
|
||||
- Created with help from the entire community.
|
||||
- Created with help from the entire community.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue