From ebeab323fbf5f02a638eb914500107265397eea4 Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 00:59:54 +0000 Subject: [PATCH 01/14] feat: user dict saved to disk every time hide keyboard --- app/src/main/java/com/osfans/trime/ime/core/Trime.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index 98437e361c..5a70594a9e 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -918,6 +918,7 @@ public void commitTextByChar(String text) { private boolean handleBack(int keyCode) { if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { requestHideSelf(0); + Rime.finalize1(); return true; } return false; From 7b48015d7830400e3c33916e360e7a2f5655bde6 Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:33:45 +0800 Subject: [PATCH 02/14] feat: destroy rime every time keyboard hide --- app/src/main/java/com/osfans/trime/ime/core/Trime.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index 5a70594a9e..6880008ca7 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -918,7 +918,9 @@ public void commitTextByChar(String text) { private boolean handleBack(int keyCode) { if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { requestHideSelf(0); - Rime.finalize1(); + Rime.destroy(); + getImeConfig().destroy(); + System.exit(0); // 清理內存 return true; } return false; From 279103bd819d219c1e2532f519d2195692c4725c Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:37:10 +0800 Subject: [PATCH 03/14] feat: update rime destroy time --- app/src/main/java/com/osfans/trime/ime/core/Trime.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index 6880008ca7..d7daeafccd 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -301,6 +301,7 @@ public void onWindowHidden() { } isWindowShown = false; + Rime.destroy(); if (getPrefs().getConf().getSyncBackgroundEnabled()) { final Message msg = new Message(); msg.obj = this; @@ -918,9 +919,6 @@ public void commitTextByChar(String text) { private boolean handleBack(int keyCode) { if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { requestHideSelf(0); - Rime.destroy(); - getImeConfig().destroy(); - System.exit(0); // 清理內存 return true; } return false; From e8de9ac7bf288f895db580e5e30662e70f46c400 Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:00:04 +0800 Subject: [PATCH 04/14] feat: update destory time to onWindowHidden --- app/src/main/java/com/osfans/trime/core/Rime.java | 6 ++++++ .../java/com/osfans/trime/ime/core/Trime.java | 15 ++++++++++++++- .../java/com/osfans/trime/util/ShortcutUtils.kt | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/osfans/trime/core/Rime.java b/app/src/main/java/com/osfans/trime/core/Rime.java index 94862df7aa..e36d843cf8 100644 --- a/app/src/main/java/com/osfans/trime/core/Rime.java +++ b/app/src/main/java/com/osfans/trime/core/Rime.java @@ -691,6 +691,12 @@ public static boolean syncUserData(Context context) { return b; } + public static boolean saveUserData(Context context) { + destroy(); + get(context, true); + return true; + } + // init public static native void setup(String shared_data_dir, String user_data_dir); diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index d7daeafccd..2167c047f5 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -258,6 +258,16 @@ public static Trime getServiceOrNull() { } return false; }); + + private static final Handler writeUserDataHandler = + new Handler( + msg -> { + if (!((Trime) msg.obj).isShowInputRequested()) { // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开 + ShortcutUtils.INSTANCE.writeUserData((Trime) msg.obj); + ((Trime) msg.obj).loadConfig(); + } + return false; + }); public Trime() { try { @@ -301,11 +311,14 @@ public void onWindowHidden() { } isWindowShown = false; - Rime.destroy(); if (getPrefs().getConf().getSyncBackgroundEnabled()) { final Message msg = new Message(); msg.obj = this; syncBackgroundHandler.sendMessageDelayed(msg, 5000); // 输入面板隐藏5秒后,开始后台同步 + } else { + final Message msg = new Message(); + msg.obj = this; + writeUserDataHandler.sendMessageDelayed(msg, 5000); // 输入面板隐藏5秒后,开始后台同步 } Timber.d(methodName + "eventListeners"); diff --git a/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt b/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt index b56a8bc258..a0cd2a6507 100644 --- a/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt +++ b/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt @@ -128,6 +128,10 @@ object ShortcutUtils { prefs.conf.lastSyncStatus = Rime.syncUserData(context) } + fun writeUserData(context: Context) { + Rime.saveUserData(context) + } + fun openCategory(keyCode: Int): Boolean { val category = applicationLaunchKeyCategories[keyCode] return if (category != null) { From 9f290748893ee54f19b2ae18212baba1f538ff85 Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:17:51 +0800 Subject: [PATCH 05/14] fix: removed unused space --- app/src/main/java/com/osfans/trime/ime/core/Trime.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index 2167c047f5..ae2de64636 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -258,7 +258,7 @@ public static Trime getServiceOrNull() { } return false; }); - + private static final Handler writeUserDataHandler = new Handler( msg -> { From e25163f060a11c16b7bb7b5eeef217775530f0d7 Mon Sep 17 00:00:00 2001 From: mokapsing <39441028+mokapsing@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:32:17 +0800 Subject: [PATCH 06/14] feat: manage data in clipboard / draft / collection by tumuyan --- app/src/main/AndroidManifest.xml | 4 + app/src/main/assets/rime/trime.yaml | 11 +- .../osfans/trime/data/db/CollectionDao.java | 92 ++++++++++ .../java/com/osfans/trime/data/db/DbBean.java | 2 +- .../trime/data/db/clipboard/ClipboardDao.java | 16 +- .../osfans/trime/data/db/draft/DraftDao.java | 22 ++- .../java/com/osfans/trime/ime/core/Trime.java | 4 +- .../trime/ime/enums/SymbolKeyboardType.kt | 3 + .../trime/ime/symbol/CheckableAdatper.java | 109 ++++++++++++ .../trime/ime/symbol/CollectionAdapter.java | 159 ++++++++++++++++++ .../trime/ime/symbol/LiquidKeyboard.java | 54 +++++- .../trime/settings/LiquidKeyboardActivity.kt | 135 +++++++++++++++ .../trime/settings/fragments/OtherFragment.kt | 22 +++ app/src/main/res/layout/checkable_item.xml | 26 +++ .../res/layout/liquid_keyboard_activity.xml | 67 ++++++++ app/src/main/res/values-zh-rCN/strings.xml | 6 + app/src/main/res/values-zh-rTW/strings.xml | 6 + app/src/main/res/xml/other_preference.xml | 11 ++ 18 files changed, 736 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/osfans/trime/data/db/CollectionDao.java create mode 100644 app/src/main/java/com/osfans/trime/ime/symbol/CheckableAdatper.java create mode 100644 app/src/main/java/com/osfans/trime/ime/symbol/CollectionAdapter.java create mode 100644 app/src/main/java/com/osfans/trime/settings/LiquidKeyboardActivity.kt create mode 100644 app/src/main/res/layout/checkable_item.xml create mode 100644 app/src/main/res/layout/liquid_keyboard_activity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aee06a25e6..938d25a3cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,6 +77,10 @@ + diff --git a/app/src/main/assets/rime/trime.yaml b/app/src/main/assets/rime/trime.yaml index 1c1e8cd65e..c3d5d70aca 100644 --- a/app/src/main/assets/rime/trime.yaml +++ b/app/src/main/assets/rime/trime.yaml @@ -693,7 +693,7 @@ liquid_keyboard: single_width: 60 #single类型的按键宽度 vertical_gap: 1 #纵向按键间隙 margin_x: 0.5 #左右按键间隙的1/2 - keyboards: [emoji, math, ascii, cn, history, clipboard, draft, tabs, exit, candidate, list, ids , table, symbol, unit, new, jp, pinyin, grease, rusa, korea, lation, yinbiao, exit] #tab列表 + keyboards: [emoji, math, ascii, cn, history, clipboard, draft, collection, tabs, exit, candidate, list, ids , table, symbol, unit, new, jp, pinyin, grease, rusa, korea, lation, yinbiao, yanwenzi, exit] #tab列表 exit: name: 返回 type: NO_KEY @@ -712,10 +712,13 @@ liquid_keyboard: type: HISTORY emoji: type: SINGLE - keys: "🙂😂🤣😆🙃😅🙈🙉🙊☹😑😄🤐😨😱🌚🌝🤔❤💔♡🌹💣👌👍😣😥😮🙄😏😕😯😪😫😴😌🤑😉😋😎😍😘😚😛😜😝😒😓😔😲😷🤒😇🤓🤗🤕🙁😖😞😟😤😢😭😦😧😨😩😬😰😳😵😡😠☝✌🖕👎🙏🤘👏💪💋☘🍀🌸☕🍵🍺🍻🍦🍬🍚🍜🍲🍖🎂💤" + keys: "🙂😂🤣😆🙃😅🥺🙈🙉🙊☹😑😄🤐😨😱🌚🌝🤔❤💔🌹💣👌👍😣😥😮🙄😏😕😯😪😫😴😌🤑😉😋😎😍😘😚😛😜😝😒😓😔😲😷🤒😇🤓🤗🤕🙁😖😞😟😤😢😭😦😧😨😩😬😰😳😵😡😠☝✌🖕👎🙏🤘👏💪💋☘🍀🌸☕🍵🍺🍻🍦🍬🍚🍜🍲🍖🎂💤" clipboard: type: CLIPBOARD name: 剪贴 + collection: + type: COLLECTION + name: 收藏 draft: type: DRAFT name: 草稿 @@ -792,6 +795,10 @@ liquid_keyboard: type: SINGLE name: "音标" keys: ["a:", "ɔ:", "ɜː", "i:", "u:", "ʌ", "ɒ", "ə", "ɪ", "ʊ", "e", "æ", "eɪ", "aɪ", "ɔɪ", "ɪə", "eə", "ʊə", "əʊ", "aʊ", "p", "t", "k", "f", "θ", "s", "b", "d", "g", "v", "ð", "z", "ʃ", "h", "ts", "tʃ", "j", "tr", "ʒ", "r", "dz", "dʒ", "dr", "w", "m", "n", "ŋ", "l"] + yanwenzi: + type: VAR_LENGTH + name: "颜文字" + keys: ["⎛⎝≥⏝⏝≤⎠⎞", "^_^", "^ω^", "^o^", "~\\(≧▽≦)/~", "*^_^*", "↖(^ω^)↗", "(^o^)/", "(=^▽^=)", "=^_^=", "(*^ω^*)", "٩(๑^o^๑)۶", "o( ̄▽ ̄)o", "Y(^_^)Y", "٩( 'ω' )و", "╰(*´︶`*)╯", "*罒▽罒*", "ヾ ^_^♪", "= ̄ω ̄=", "︿( ̄︶ ̄)︿", "(´▽`)ノ♪", "乁( ˙ ω˙乁)", "✧*。٩(ˊωˋ*)و✧*。", "~( ̄▽ ̄~)(~ ̄▽ ̄)~", "QwQ", "(●—●)", "(๑• . •๑)", "ヾ(≧O≦)〃嗷~", "罒ω罒", "(。ì _ í。)", "(๑•ี_เ•ี๑)", "ㄟ(≧◇≦)ㄏ", "(*/ω\*)", "●▽●", "٩(๑òωó๑)۶", "✺◟(∗❛ัᴗ❛ั∗)◞✺", "( σ'ω')σ", "♡^▽^♡", "(๑•̀ㅂ•́)و✧", "(ง •̀_•́)ง", "(。・ω・。)ノ♡", "(☆_☆)", "(๑°3°๑)", "_(•̀ω•́ 」∠)_", "♪~(´ε` )", "~(^з^)-☆", "(´∀`)♡", "ლ(´ڡ`ლ)", "(>﹏<)", "T_T", "⊙︿⊙", "〒▽〒", "⊙﹏⊙", "π_π", "(。•́︿•̀。)", "(ToT)/~~~", "╯﹏╰", "ಥ_ಥ", "(╥╯^╰╥)", "(〃′o`)", "●﹏●", "( •̥́ ˍ •̀ू )", "(つд⊂)", "心塞(´-ωก`)", "(╥﹏╥)", "┭┮﹏┭┮", "(;´д`)ゞ", "(´;︵;`)", "(。﹏。)", "┗( T﹏T )┛", "QAQ", "ヘ(_ _ヘ)", "╰(‵□′)╯", "(* ̄︿ ̄)", ">o<", "(-`ェ´-怒)", "ヽ(‘⌒´メ)ノ", "(*`Ω´*)v", "(。•ˇ‸ˇ•。)", "(怒`Д´怒)", "٩(๑`^´๑)۶", "(;`O´)o", "╰_╯", "(#`皿´)<怒怒怒!!!", "<(`^´)>", "(`Δ´)!", "ψ(`∇´)ψ", "(;′⌒`)", "s(・`ヘ´・;)ゞ", "(▼皿▼#)", " ̄へ ̄", "←_←", "(╯‵□′)╯︵┴─┴", "(▼へ▼メ)", "☄ฺ(◣д◢)☄ฺ", "→_→", "⊙_⊙", "d(ŐдŐ๑)", "Σ( ° △ °|||)︴", "(((φ(◎ロ◎;)φ)))", "⊙▽⊙", "(๑ŐдŐ)b", "╭(°A°`)╮", "(๑òᆺó๑)", "⊙ω⊙", "Σ(っ °Д °;)っ", " (゚Д゚≡゚д゚)!?", "( *・ω・)✄╰ひ╯", "(⊙x⊙;)", "┌(。Д。)┐", "(°ー°〃)", "︽⊙_⊙︽", "!!!∑(°Д°ノ)ノ", "(๑ŐдŐ)b☆d(ŐдŐ๑)", "Σ⊙▃⊙川", "ヽ(*。>Д<)o゜", "━((*′д`)爻(′д`*))━!!!!", "=_=", "╮(╯_╰)╭", "(︶︿︶)", "_(:з」∠)_", "@_@", "╮(╯▽╰)╭", "(@ ̄ー ̄@)", "_(:3」∠❀)_", "눈_눈", "╭(╯ε╰)╮", "(ー_ー)!!", "_(:D)∠)_", "o_O", "╭(╯^╰)╮", "(;一_一)", "´_>`", "-_-#", "┑( ̄Д  ̄)┍", "≡ ̄﹏ ̄≡", "○| ̄|_", "-_-||", "ㄟ( ▔, ▔ )ㄏ", "( _ _)ノ|壁", "▄█▀█●"] ascii: type: SINGLE name: 英文 diff --git a/app/src/main/java/com/osfans/trime/data/db/CollectionDao.java b/app/src/main/java/com/osfans/trime/data/db/CollectionDao.java new file mode 100644 index 0000000000..62cb4cf9cb --- /dev/null +++ b/app/src/main/java/com/osfans/trime/data/db/CollectionDao.java @@ -0,0 +1,92 @@ +package com.osfans.trime.data.db; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.NonNull; +import com.osfans.trime.ime.core.Trime; +import com.osfans.trime.ime.symbol.SimpleKeyBean; +import java.util.ArrayList; +import java.util.List; +import timber.log.Timber; + +public class CollectionDao { + + private DbHelper helper; + private static CollectionDao self; + private static String DB_NAME = "collection.db"; + + public static CollectionDao get() { + if (null == self) self = new CollectionDao(); + return self; + } + + public CollectionDao() {} + + /** 插入新记录 * */ + public void insert(@NonNull DbBean clipboardBean) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + db.execSQL( + "insert into t_data(text,html,type,time) values(?,?,?,?)", + new Object[] { + clipboardBean.getText(), + clipboardBean.getHtml(), + clipboardBean.getType(), + clipboardBean.getTime() + }); + db.close(); + } + + /** 删除文字相同的剪贴板记录,插入新记录 * */ + public void add(@NonNull DbBean clipboardBean) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + db.delete("t_data", "text=?", new String[] {clipboardBean.getText()}); + db.execSQL( + "insert into t_data(text,html,type,time) values(?,?,?,?)", + new Object[] { + clipboardBean.getText(), + clipboardBean.getHtml(), + clipboardBean.getType(), + clipboardBean.getTime() + }); + db.close(); + } + + /** 删除记录 * */ + public void delete(@NonNull String str) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + db.delete("t_data", "text=?", new String[] {str}); + db.close(); + } + + public void delete(@NonNull List list) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + for (SimpleKeyBean bean : list) db.delete("t_data", "text=?", new String[] {bean.getText()}); + db.close(); + } + + public List getAllSimpleBean() { + + List list = new ArrayList<>(); + + String sql = "select text , html , type , time from t_data ORDER BY time DESC"; + + helper = new DbHelper(Trime.getService(), DB_NAME); + + SQLiteDatabase db = helper.getWritableDatabase(); + Cursor cursor = db.rawQuery(sql, null); + if (cursor != null) { + while (cursor.moveToNext()) { + DbBean v = new DbBean(cursor.getString(0)); + list.add(v); + } + cursor.close(); + } + db.close(); + Timber.d("getAllSimpleBean() size=%s", list.size()); + return list; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/osfans/trime/data/db/DbBean.java b/app/src/main/java/com/osfans/trime/data/db/DbBean.java index 72e52e98f6..95e5705abb 100644 --- a/app/src/main/java/com/osfans/trime/data/db/DbBean.java +++ b/app/src/main/java/com/osfans/trime/data/db/DbBean.java @@ -6,7 +6,7 @@ public class DbBean extends SimpleKeyBean { private long time; private String text; private final String html; - private int type; + private int type; // 0 text, 1 html public long getTime() { return time; diff --git a/app/src/main/java/com/osfans/trime/data/db/clipboard/ClipboardDao.java b/app/src/main/java/com/osfans/trime/data/db/clipboard/ClipboardDao.java index 621b4aa61c..25ad1a8f0e 100644 --- a/app/src/main/java/com/osfans/trime/data/db/clipboard/ClipboardDao.java +++ b/app/src/main/java/com/osfans/trime/data/db/clipboard/ClipboardDao.java @@ -15,6 +15,7 @@ public class ClipboardDao { private DbHelper helper; private static ClipboardDao self; + private static String DB_NAME = "clipboard.db"; public static ClipboardDao get() { if (null == self) self = new ClipboardDao(); @@ -25,7 +26,7 @@ public ClipboardDao() {} /** 插入新记录 * */ public void insert(@NonNull DbBean clipboardBean) { - helper = new DbHelper(Trime.getService(), "clipboard.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL( "insert into t_data(text,html,type,time) values(?,?,?,?)", @@ -40,7 +41,7 @@ public void insert(@NonNull DbBean clipboardBean) { /** 删除文字相同的剪贴板记录,插入新记录 * */ public void add(@NonNull DbBean clipboardBean) { - helper = new DbHelper(Trime.getService(), "clipboard.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); db.delete("t_data", "text=?", new String[] {clipboardBean.getText()}); db.execSQL( @@ -56,12 +57,19 @@ public void add(@NonNull DbBean clipboardBean) { /** 删除记录 * */ public void delete(@NonNull String str) { - helper = new DbHelper(Trime.getService(), "clipboard.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); db.delete("t_data", "text=?", new String[] {str}); db.close(); } + public void delete(@NonNull List list) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + for (SimpleKeyBean bean : list) db.delete("t_data", "text=?", new String[] {bean.getText()}); + db.close(); + } + public List getAllSimpleBean(int size) { List list = new ArrayList<>(); @@ -70,7 +78,7 @@ public List getAllSimpleBean(int size) { String sql = "select text , html , type , time from t_data ORDER BY time DESC"; if (size > 0) sql = sql + " limit 0," + size; - helper = new DbHelper(Trime.getService(), "clipboard.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); Cursor cursor = db.rawQuery(sql, null); diff --git a/app/src/main/java/com/osfans/trime/data/db/draft/DraftDao.java b/app/src/main/java/com/osfans/trime/data/db/draft/DraftDao.java index fac654aef0..f9aa91129e 100644 --- a/app/src/main/java/com/osfans/trime/data/db/draft/DraftDao.java +++ b/app/src/main/java/com/osfans/trime/data/db/draft/DraftDao.java @@ -16,6 +16,7 @@ public class DraftDao { private SQLiteOpenHelper helper; private static DraftDao self; + private static String DB_NAME = "draft.db"; public static DraftDao get() { if (null == self) self = new DraftDao(); @@ -26,7 +27,7 @@ public DraftDao() {} /** 插入新记录 * */ public void insert(@NonNull DbBean bean) { - helper = new DbHelper(Trime.getService(), "draft.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL( "insert into t_data(text,html,type,time) values(?,?,?,?)", @@ -36,7 +37,7 @@ public void insert(@NonNull DbBean bean) { /** 删除文字相同的记录,插入新记录 * */ public void add(@NonNull DbBean bean) { - helper = new DbHelper(Trime.getService(), "draft.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); db.delete("t_data", "text=?", new String[] {bean.getText()}); db.execSQL( @@ -45,6 +46,21 @@ public void add(@NonNull DbBean bean) { db.close(); } + /** 删除记录 * */ + public void delete(@NonNull String str) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + db.delete("t_data", "text=?", new String[] {str}); + db.close(); + } + + public void delete(@NonNull List list) { + helper = new DbHelper(Trime.getService(), DB_NAME); + SQLiteDatabase db = helper.getWritableDatabase(); + for (SimpleKeyBean bean : list) db.delete("t_data", "text=?", new String[] {bean.getText()}); + db.close(); + } + public List getAllSimpleBean(int size) { List list = new ArrayList<>(); @@ -53,7 +69,7 @@ public List getAllSimpleBean(int size) { String sql = "select text , html , type , time from t_data ORDER BY time DESC"; if (size > 0) sql = sql + " limit 0," + size; - helper = new DbHelper(Trime.getService(), "draft.db"); + helper = new DbHelper(Trime.getService(), DB_NAME); SQLiteDatabase db = helper.getWritableDatabase(); Cursor cursor = db.rawQuery(sql, null); diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java index ae2de64636..9fa55a48d9 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.java @@ -62,6 +62,7 @@ import com.osfans.trime.core.Rime; import com.osfans.trime.data.AppPrefs; import com.osfans.trime.data.Config; +import com.osfans.trime.data.db.CollectionDao; import com.osfans.trime.data.db.clipboard.ClipboardDao; import com.osfans.trime.data.db.draft.DraftDao; import com.osfans.trime.databinding.CompositionRootBinding; @@ -237,9 +238,7 @@ public void run() { }; @Synchronized - @NonNull public static Trime getService() { - assert self != null; return self; } @@ -405,6 +404,7 @@ public void onCreate() { new LiquidKeyboard( this, getImeConfig().getClipboardLimit(), getImeConfig().getDraftLimit()); clipBoardMonitor(); + CollectionDao.get(); DraftDao.get(); } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/osfans/trime/ime/enums/SymbolKeyboardType.kt b/app/src/main/java/com/osfans/trime/ime/enums/SymbolKeyboardType.kt index b77945efb3..2f2bdb400d 100644 --- a/app/src/main/java/com/osfans/trime/ime/enums/SymbolKeyboardType.kt +++ b/app/src/main/java/com/osfans/trime/ime/enums/SymbolKeyboardType.kt @@ -16,6 +16,9 @@ enum class SymbolKeyboardType { // 剪贴板(大段文本自动缩略,按键长度自适应。) CLIPBOARD, + // 收藏的文本, 复用剪贴板 + COLLECTION, + // 文本框编辑历史,即“草稿箱” DRAFT, diff --git a/app/src/main/java/com/osfans/trime/ime/symbol/CheckableAdatper.java b/app/src/main/java/com/osfans/trime/ime/symbol/CheckableAdatper.java new file mode 100644 index 0000000000..1680022f60 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/symbol/CheckableAdatper.java @@ -0,0 +1,109 @@ +package com.osfans.trime.ime.symbol; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.TextView; +import androidx.annotation.NonNull; +import com.osfans.trime.R; +import com.osfans.trime.data.db.CollectionDao; +import com.osfans.trime.data.db.DbBean; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CheckableAdatper extends ArrayAdapter { + private LayoutInflater layoutInflater = null; + private List words; + + public List getChecked() { + return checked; + } + + private List checked; + + public CheckableAdatper(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + layoutInflater = LayoutInflater.from(context); + words = objects; + checked = new ArrayList<>(); + Log.i("UserDictAdatper", "set words.size=" + words.size()); + } + + public void clickItem(int position) { + if (checked.contains(position)) checked.remove((Integer) position); + else checked.add(position); + + notifyDataSetChanged(); + } + + public List remove(int checkPositon) { + Collections.sort(checked, Collections.reverseOrder()); + List result = new ArrayList<>(); + for (int i : checked) { + if (i > words.size()) continue; + result.add(words.get(i)); + words.remove(i); + } + checked.clear(); + + if (checkPositon >= 0) checked.add(checkPositon); + + notifyDataSetChanged(); + return result; + } + + public List collectSelected() { + Collections.sort(checked, Collections.reverseOrder()); + List result = new ArrayList<>(); + for (int i : checked) { + if (i > words.size()) continue; + SimpleKeyBean bean = words.get(i); + result.add(bean); + CollectionDao.get().insert(new DbBean(bean.getText())); + } + return result; + } + + @Override + public int getCount() { + return words.size(); + } + + @Override + public Object getItem(int position) { + return words.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + private static class ViewHolder { + private TextView textView; + private CheckBox checkBox; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // 优化后 + ViewHolder holder; + if (convertView == null) { + convertView = layoutInflater.inflate(R.layout.checkable_item, null); + holder = new ViewHolder(); + holder.textView = (TextView) convertView.findViewById(R.id.textView); + holder.checkBox = (CheckBox) convertView.findViewById(R.id.checkBox); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + holder.checkBox.setText(words.get(position).getText()); + holder.checkBox.setChecked(checked.contains(position)); + return convertView; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/osfans/trime/ime/symbol/CollectionAdapter.java b/app/src/main/java/com/osfans/trime/ime/symbol/CollectionAdapter.java new file mode 100644 index 0000000000..0d6723fa20 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/symbol/CollectionAdapter.java @@ -0,0 +1,159 @@ +package com.osfans.trime.ime.symbol; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.blankj.utilcode.util.ToastUtils; +import com.google.android.flexbox.FlexboxLayoutManager; +import com.osfans.trime.R; +import com.osfans.trime.data.Config; +import java.util.List; + +public class CollectionAdapter extends RecyclerView.Adapter { + private final Context myContext; + private final List list; + private int keyMarginX, keyMarginTop; + private Integer textColor; + private float textSize; + private Typeface textFont; + private Config config; + + public CollectionAdapter(Context context, List itemlist) { + myContext = context; + list = itemlist; + } + + @Override + public int getItemCount() { + return list.size(); + } + + public void configStyle(int keyMarginX, int keyMarginTop) { + this.keyMarginX = keyMarginX; + this.keyMarginTop = keyMarginTop; + + // 边框尺寸、圆角、字号直接读取主题通用参数。配色优先读取 liquidKeyboard 专用参数。 + config = Config.get(myContext); + textColor = config.getLiquidColor("long_text_color"); + if (textColor == null) textColor = config.getLiquidColor("key_text_color"); + + textSize = config.getFloat("key_long_text_size"); + if (textSize <= 0) textSize = config.getFloat("label_text_size"); + + textFont = config.getFont("long_text_font"); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(myContext).inflate(R.layout.simple_key_item, parent, false); + return new ItemViewHolder(view); + } + + static class ItemViewHolder extends RecyclerView.ViewHolder { + public ItemViewHolder(View view) { + super(view); + + listItemLayout = view.findViewById(R.id.listitem_layout); + mTitle = view.findViewById(R.id.simple_key); + } + + LinearLayout listItemLayout; + TextView mTitle; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int index) { + + if (viewHolder instanceof CollectionAdapter.ItemViewHolder) { + SimpleKeyBean searchHistoryBean = list.get(index); + final CollectionAdapter.ItemViewHolder itemViewHold = + ((CollectionAdapter.ItemViewHolder) viewHolder); + + if (textFont != null) itemViewHold.mTitle.setTypeface(textFont); + + String text = searchHistoryBean.getText(); + if (text.length() > 300) itemViewHold.mTitle.setText(text.substring(0, 300)); + else itemViewHold.mTitle.setText(text); + + if (textSize > 0) itemViewHold.mTitle.setTextSize(textSize); + + ViewGroup.LayoutParams lp = itemViewHold.listItemLayout.getLayoutParams(); + if (lp instanceof FlexboxLayoutManager.LayoutParams) { + FlexboxLayoutManager.LayoutParams flexboxLp = + (FlexboxLayoutManager.LayoutParams) itemViewHold.listItemLayout.getLayoutParams(); + + itemViewHold.mTitle.setTextColor(textColor); + + int marginTop = flexboxLp.getMarginTop(); + int marginX = flexboxLp.getMarginLeft(); + if (keyMarginTop > 0) marginTop = keyMarginTop; + if (keyMarginX > 0) marginX = keyMarginX; + + flexboxLp.setMargins(marginX, marginTop, marginX, flexboxLp.getMarginBottom()); + + // TODO 设置剪贴板列表样式 + // copy SimpleAdapter 会造成高度始终为 3 行无法自适应的效果。 + + } + + Drawable background = + config.getDrawable( + "long_text_back_color", "key_border", "key_long_text_border", "round_corner", null); + + if (background != null) itemViewHold.listItemLayout.setBackground(background); + // 如果设置了回调,则设置点击事件 + if (mOnItemClickListener != null) { + itemViewHold.listItemLayout.setOnClickListener( + view -> { + int position = itemViewHold.getLayoutPosition(); + mOnItemClickListener.onItemClick(itemViewHold.listItemLayout, position); + }); + } + + itemViewHold.listItemLayout.setOnLongClickListener( + view -> { + int position = itemViewHold.getLayoutPosition(); + // TODO 长按删除、编辑剪贴板 + // 当文本较长时,目前样式只缩略显示为 3 行,长按时 toast 消息可以预览全文,略有用处。 + ToastUtils.showShort(list.get(position).getText()); + return true; + }); + + // TODO 剪贴板列表点击时产生背景变色效果 + itemViewHold.listItemLayout.setOnTouchListener( + (view, motionEvent) -> { + int action = motionEvent.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + break; + } + return false; + }); + } + } + + /** 添加 OnItemClickListener 回调 * */ + public interface OnItemClickListener { + void onItemClick(View view, int position); + } + + private OnItemClickListener mOnItemClickListener; + + public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) { + this.mOnItemClickListener = mOnItemClickListener; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/osfans/trime/ime/symbol/LiquidKeyboard.java b/app/src/main/java/com/osfans/trime/ime/symbol/LiquidKeyboard.java index 0298adcb34..6c571bb427 100644 --- a/app/src/main/java/com/osfans/trime/ime/symbol/LiquidKeyboard.java +++ b/app/src/main/java/com/osfans/trime/ime/symbol/LiquidKeyboard.java @@ -12,6 +12,7 @@ import com.osfans.trime.R; import com.osfans.trime.core.Rime; import com.osfans.trime.data.Config; +import com.osfans.trime.data.db.CollectionDao; import com.osfans.trime.data.db.DbBean; import com.osfans.trime.data.db.clipboard.ClipboardDao; import com.osfans.trime.data.db.draft.DraftDao; @@ -31,10 +32,11 @@ public class LiquidKeyboard { private RecyclerView keyboardView; private LinearLayout parentView; private ClipboardAdapter mClipboardAdapter; + private CollectionAdapter mCollectionAdapter; private DraftAdapter mDraftAdapter; private SimpleAdapter simpleAdapter; private CandidateAdapter candidateAdapter; - private List clipboardBeanList, draftBeanList; + private List clipboardBeanList, collectionBeanList, draftBeanList; private final List simpleKeyBeans; private List historyBeans; private int margin_x, margin_top, single_width, parent_width, clipboard_max_size, draft_max_size; @@ -60,6 +62,9 @@ public LiquidKeyboard(Context context, int clipboard_max_size, int draft_max_siz clipboardBeanList = ClipboardDao.get().getAllSimpleBean(clipboard_max_size); Timber.d("clipboardBeanList.size=%s", clipboardBeanList.size()); + collectionBeanList = CollectionDao.get().getAllSimpleBean(); + Timber.d("collectionBeanList.size=%s", collectionBeanList.size()); + draftBeanList = DraftDao.get().getAllSimpleBean(draft_max_size); Timber.d("draftBeanList.size=%s", draftBeanList.size()); @@ -75,6 +80,13 @@ public void addClipboardData(String text) { if (mClipboardAdapter != null) mClipboardAdapter.notifyItemInserted(0); } + public void addCollectionData(String text) { + DbBean bean = new DbBean(text); + CollectionDao.get().add(bean); + collectionBeanList.add(0, bean); + if (mCollectionAdapter != null) mCollectionAdapter.notifyItemInserted(0); + } + public void addDraftData(String text) { DbBean bean = new DbBean(text); DraftDao.get().add(bean); @@ -91,6 +103,10 @@ public SymbolKeyboardType select(int i) { TabManager.get().select(i); initClipboardData(); break; + case COLLECTION: + TabManager.get().select(i); + initCollectionData(); + break; case DRAFT: TabManager.get().select(i); initDraftData(); @@ -197,6 +213,7 @@ private void calcPadding(SymbolKeyboardType type) { public void initFixData(int i) { keyboardView.removeAllViews(); mClipboardAdapter = null; + mCollectionAdapter = null; mDraftAdapter = null; // 设置布局管理器 FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(context); @@ -275,6 +292,7 @@ public void initFixData(int i) { public void initClipboardData() { keyboardView.removeAllViews(); simpleAdapter = null; + mCollectionAdapter = null; // 设置布局管理器 FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(context); @@ -307,6 +325,39 @@ public void initClipboardData() { }); } + public void initCollectionData() { + keyboardView.removeAllViews(); + simpleAdapter = null; + mClipboardAdapter = null; + // 设置布局管理器 + FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(context); + // flexDirection 属性决定主轴的方向(即项目的排列方向)。类似 LinearLayout 的 vertical 和 horizontal。 + flexboxLayoutManager.setFlexDirection(FlexDirection.ROW); // 主轴为水平方向,起点在左端。 + // flexWrap 默认情况下 Flex 跟 LinearLayout 一样,都是不带换行排列的,但是flexWrap属性可以支持换行排列。 + flexboxLayoutManager.setFlexWrap(FlexWrap.WRAP); // 按正常方向换行 + // justifyContent 属性定义了项目在主轴上的对齐方式。 + flexboxLayoutManager.setJustifyContent(JustifyContent.FLEX_START); // 交叉轴的起点对齐。 + // flexboxLayoutManager.setAlignItems(AlignItems.BASELINE); + keyboardView.setLayoutManager(flexboxLayoutManager); + + collectionBeanList = CollectionDao.get().getAllSimpleBean(); + mCollectionAdapter = new CollectionAdapter(context, collectionBeanList); + + mCollectionAdapter.configStyle(margin_x, margin_top); + + keyboardView.setAdapter(mCollectionAdapter); + // 调用ListView的setSelected(!ListView.isSelected())方法,这样就能及时刷新布局 + keyboardView.setSelected(true); + + mCollectionAdapter.setOnItemClickListener( + (view, position) -> { + InputConnection ic = Trime.getService().getCurrentInputConnection(); + if (ic != null) { + ic.commitText(collectionBeanList.get(position).getText(), 1); + } + }); + } + public void initDraftData() { keyboardView.removeAllViews(); simpleAdapter = null; @@ -347,6 +398,7 @@ public void initCandidateAdapter() { simpleAdapter = null; mClipboardAdapter = null; mDraftAdapter = null; + mCollectionAdapter = null; if (candidateAdapter == null) candidateAdapter = new CandidateAdapter(context); diff --git a/app/src/main/java/com/osfans/trime/settings/LiquidKeyboardActivity.kt b/app/src/main/java/com/osfans/trime/settings/LiquidKeyboardActivity.kt new file mode 100644 index 0000000000..f95ce75c6e --- /dev/null +++ b/app/src/main/java/com/osfans/trime/settings/LiquidKeyboardActivity.kt @@ -0,0 +1,135 @@ +package com.osfans.trime.ui.main + +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import com.blankj.utilcode.util.BarUtils +import com.osfans.trime.R +import com.osfans.trime.data.db.CollectionDao +import com.osfans.trime.data.db.clipboard.ClipboardDao +import com.osfans.trime.data.db.draft.DraftDao +import com.osfans.trime.databinding.LiquidKeyboardActivityBinding +import com.osfans.trime.ime.symbol.CheckableAdatper +import com.osfans.trime.ime.symbol.SimpleKeyBean +import timber.log.Timber + +class LiquidKeyboardActivity : AppCompatActivity() { + val CLIPBOARD = "clipboard" + val COLLECTION = "collection" + val DRAFT = "draft" + lateinit var binding: LiquidKeyboardActivityBinding + var type: String = CLIPBOARD + var beans: List = ArrayList() + lateinit var mAdapter: CheckableAdatper + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + binding = LiquidKeyboardActivityBinding.inflate(layoutInflater) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + BarUtils.setNavBarColor( + this, + getColor(R.color.windowBackground) + ) + } else + BarUtils.setNavBarColor( + this, + @Suppress("DEPRECATION") + resources.getColor(R.color.windowBackground) + ) + setContentView(binding.root) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + + type = intent.getStringExtra("type").toString() + if (type.equals(COLLECTION)) { + setTitle(R.string.other__list_collection_title) + } else if (type.equals(DRAFT)) { + setTitle(R.string.other__list_draft_title) + } else { + type = CLIPBOARD + setTitle(R.string.other__list_clipboard_title) + } + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + getDbData() + + binding.btnDel.setOnClickListener { + + var position: Int = + binding.listWord.getPositionForView(binding.listWord.getChildAt(0)) + if (position > 0) position++ + val r: List = mAdapter.remove(position) + if (r.isNotEmpty()) { + if (type.equals(COLLECTION)) + CollectionDao.get().delete(r) + else if (type.equals(DRAFT)) + DraftDao.get().delete(r) + else + ClipboardDao.get().delete(r) + + Timber.d("delete " + r.size) + } + } + + binding.btnCollect.setOnClickListener { + + Timber.d("collect") + mAdapter.collectSelected() + } + } + + override fun onSupportNavigateUp(): Boolean { + if (supportFragmentManager.popBackStackImmediate()) { + return true + } + return super.onSupportNavigateUp() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + return true + } + + fun getDbData() { + binding.progressBar.setVisibility(View.VISIBLE) + + if (type.equals(COLLECTION)) { + beans = CollectionDao.get().allSimpleBean + binding.btnCollect.setVisibility(View.GONE) + } else if (type.equals(DRAFT)) { + beans = DraftDao.get().getAllSimpleBean(1000) + } else { + beans = ClipboardDao.get().getAllSimpleBean(1000) + } + + mAdapter = CheckableAdatper( + this, + R.layout.checkable_item, + beans + ) + binding.listWord.setAdapter(mAdapter) + + binding.listWord.setOnItemClickListener({ parent: AdapterView<*>?, view: View?, i: Int, l: Long -> + mAdapter.clickItem( + i + ) + }) + binding.progressBar.setVisibility(View.GONE) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/osfans/trime/settings/fragments/OtherFragment.kt b/app/src/main/java/com/osfans/trime/settings/fragments/OtherFragment.kt index d115495c9c..417536e701 100644 --- a/app/src/main/java/com/osfans/trime/settings/fragments/OtherFragment.kt +++ b/app/src/main/java/com/osfans/trime/settings/fragments/OtherFragment.kt @@ -2,17 +2,21 @@ package com.osfans.trime.settings.fragments import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Bundle import android.view.Menu import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.forEach +import androidx.preference.Preference import androidx.preference.ListPreference +import com.blankj.utilcode.util.ToastUtils import androidx.preference.PreferenceFragmentCompat import com.osfans.trime.R import com.osfans.trime.data.AppPrefs import com.osfans.trime.data.Config +import com.osfans.trime.ui.main.LiquidKeyboardActivity import com.osfans.trime.ime.core.Trime class OtherFragment : @@ -69,6 +73,24 @@ class OtherFragment : } } + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + val key = preference?.key + if (key == "other__list_clipboard" || + key == "other__list_collection" || + key == "other__list_draft" + ) { + if (Trime.getService() == null) + ToastUtils.showShort(R.string.setup__select_ime_hint) + else { + val intent = Intent(this.context, LiquidKeyboardActivity::class.java) + intent.putExtra("type", key.replace("other__list_", "")) + startActivity(intent) + } + return true + } + return super.onPreferenceTreeClick(preference) + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/res/layout/checkable_item.xml b/app/src/main/res/layout/checkable_item.xml new file mode 100644 index 0000000000..b869056c76 --- /dev/null +++ b/app/src/main/res/layout/checkable_item.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/liquid_keyboard_activity.xml b/app/src/main/res/layout/liquid_keyboard_activity.xml new file mode 100644 index 0000000000..769d40ae4d --- /dev/null +++ b/app/src/main/res/layout/liquid_keyboard_activity.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + +