diff --git a/app/build.gradle b/app/build.gradle index 230aa03..8e4664b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { defaultConfig { applicationId "top.fumiama.simpledict" - minSdkVersion 23 + minSdkVersion 26 targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -62,6 +62,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' @@ -93,7 +94,8 @@ andResGuard { "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", - "R.string.google_crash_reporting_api_key" + "R.string.google_crash_reporting_api_key", + "R.font.*" ] compressFilePattern = [ "*.png", diff --git a/app/src/main/java/top/fumiama/simpledict/Client.kt b/app/src/main/java/top/fumiama/simpledict/Client.kt index d3768ae..cb16337 100644 --- a/app/src/main/java/top/fumiama/simpledict/Client.kt +++ b/app/src/main/java/top/fumiama/simpledict/Client.kt @@ -5,7 +5,6 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.net.Socket -import java.nio.charset.Charset class Client(val ip: String, val port: Int) { //普通数据交互接口 @@ -58,24 +57,27 @@ class Client(val ip: String, val port: Int) { } } - fun receiveMessage(): String? { - var message: String? = "" - try { + fun receiveRawMessage()= try { if (isConnect) { Log.d("MyC", "开始接收服务端信息") val inMessage = ByteArray(1024) //设置接受缓冲,避免接受数据过长占用过多内存 val a = din?.read(inMessage) //a存储返回消息的长度 - if (a == null || a <= -1) return null - Log.d("MyC", "reply length:$a") - message = inMessage.copyOf(a).decodeToString() - Log.d("MyC", message) - } else Log.d("MyC", "no connect to receive message") + if (a == null || a <= -1) null + else { + Log.d("MyC", "reply length:$a") + inMessage.copyOf(a) + } + } else { + Log.d("MyC", "no connect to receive message") + null + } } catch (e: IOException) { Log.d("MyC", "receive message failed") e.printStackTrace() + null } - return message - } + + fun receiveMessage() = receiveRawMessage()?.decodeToString() /** * 关闭连接 diff --git a/app/src/main/java/top/fumiama/simpledict/MainActivity.kt b/app/src/main/java/top/fumiama/simpledict/MainActivity.kt index ca26f73..0994117 100644 --- a/app/src/main/java/top/fumiama/simpledict/MainActivity.kt +++ b/app/src/main/java/top/fumiama/simpledict/MainActivity.kt @@ -1,91 +1,79 @@ package top.fumiama.simpledict import android.annotation.SuppressLint -import android.content.SharedPreferences +import android.content.Intent import android.os.Bundle +import android.speech.RecognizerIntent import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.EditText -import android.widget.TextView -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.lapism.search.internal.SearchLayout import com.lapism.search.util.SearchUtils import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.line_word.view.* -import java.lang.Thread.sleep class MainActivity : AppCompatActivity() { - private var keys = arrayOf() - private var datas = arrayOf() - private val dict = SimpleDict(Client("192.168.98.2", 8000)) + private val dict = SimpleDict(Client("pan.fumiama.top", 43792), "fumiama") + private var hasLiked = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val ad = LikeViewHolder(ffr).RecyclerViewAdapter() + ffr.apply { + layoutManager = LinearLayoutManager(this@MainActivity) + adapter = ad + setOnScrollChangeListener { _, _, scrollY, _, _ -> + ffsw.isEnabled = scrollY == 0 + } + ffsw.apply { + setOnRefreshListener { + ad.refresh() + isRefreshing = false + } + } + } ffms.apply { setAdapterLayoutManager(LinearLayoutManager(this@MainActivity)) - val adapter = ViewData(findViewById(R.id.search_recycler_view)).RecyclerViewAdapter() + val adapter = SearchViewHolder(findViewById(R.id.search_recycler_view)).RecyclerViewAdapter() setAdapter(adapter) navigationIconSupport = SearchLayout.NavigationIconSupport.SEARCH setOnNavigationClickListener(object : SearchLayout.OnNavigationClickListener { override fun onNavigationClick(hasFocus: Boolean) { - if (hasFocus()) clearFocus() + if (hasFocus()) { + if(hasLiked) ad.refresh() + clearFocus() + } else requestFocus() } }) setTextHint(android.R.string.search_go) setOnQueryTextListener(object : SearchLayout.OnQueryTextListener { - val sysTime get() = System.currentTimeMillis() / 1000 - var lastVisitTime = sysTime - val isLast get() = sysTime - lastVisitTime > 1 - var hasLoad = true - var key: CharSequence = "" - set(value) { - field = value - lastVisitTime = sysTime - hasLoad = false - } - - init { - Thread { - while (true) { - sleep(1) - if (isLast && !hasLoad) { - adapter.filter(key) - hasLoad = true - } - } - }.start() - } - override fun onQueryTextChange(newText: CharSequence): Boolean { - if (newText.isNotEmpty()) key = newText + if (newText.isNotEmpty()) adapter.filter(newText) return true } override fun onQueryTextSubmit(query: CharSequence): Boolean { - if(query.isNotEmpty()) Thread{ - val data = dict[query] - runOnUiThread { - showDictAlert(query.toString(), data) - } - }.start() + if(query.isNotEmpty()) { + val key = query.toString() + val data = dict[key] + showDictAlert(key, data) + } return true } }) setOnMicClickListener(object : SearchLayout.OnMicClickListener { override fun onMicClick() { if (SearchUtils.isVoiceSearchAvailable(this@MainActivity)) { - SearchUtils.setVoiceSearch(this@MainActivity, "speak") + SearchUtils.setVoiceSearch(this@MainActivity, "please speak") } } }) @@ -99,11 +87,28 @@ class MainActivity : AppCompatActivity() { } override fun onBackPressed() { - if(ffms.hasFocus()) ffms.clearFocus() + if(ffms.hasFocus()) { + if(hasLiked) (ffr.adapter as ListViewHolder.RecyclerViewAdapter).refresh() + ffms.clearFocus() + } else super.onBackPressed() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when(requestCode) { + SearchUtils.SPEECH_REQUEST_CODE -> data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.let { + if(it.isNotEmpty()) { + ffms.requestFocus() + ffms.mSearchEditText?.setText(it[0]) + } + } + } + } + private fun showDictAlert(key: String, data: String?) { + val like = getSharedPreferences("dict", MODE_PRIVATE)?.contains(key)?:false + hasLiked = false AlertDialog.Builder(this@MainActivity) .setTitle(key) .setMessage(data) @@ -120,55 +125,82 @@ class MainActivity : AppCompatActivity() { .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } - .setNeutralButton("收藏") { _, _ -> - getSharedPreferences("dict", MODE_PRIVATE)?.edit()?.let { - it.putString(key, data) - it.apply() + .setNeutralButton(if(like) "取消收藏" else "收藏") { _, _ -> + getSharedPreferences("dict", MODE_PRIVATE)?.edit()?.apply { + if(like) remove(key) else putString(key, data) + hasLiked = true + apply() } } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } - inner class ViewData(itemView: View) : RecyclerView.ViewHolder(itemView) { - inner class RecyclerViewAdapter : - RecyclerView.Adapter() { - var count = 0 - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewData { - return ViewData( + inner class SearchViewHolder(itemView: View) : ListViewHolder(itemView) { + inner class RecyclerViewAdapter : ListViewHolder.RecyclerViewAdapter() { + override fun getValue(key: String) = dict[key] + fun filter(text: CharSequence) { + Thread{ + val selectSet = dict.keys.filter { it.contains(text, true) }.toSet() + + dict.filterValues { it?.contains(text, true)?:false }.let { + val newSet = mutableSetOf() + it.keys.forEach { + newSet += it + } + newSet + } + listKeys = selectSet.toList() + listKeys?.forEach { + Log.d("MyMain", "Select key: $it") + } + runOnUiThread { notifyDataSetChanged() } + }.start() + } + } + } + + inner class LikeViewHolder(itemView: View) : ListViewHolder(itemView) { + inner class RecyclerViewAdapter: ListViewHolder.RecyclerViewAdapter(){ + override fun getKeys() = getSharedPreferences("dict", MODE_PRIVATE).all.keys.toList() + override fun getValue(key: String) = getSharedPreferences("dict", MODE_PRIVATE).getString(key, "null") + } + } + + open inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + open inner class RecyclerViewAdapter : + RecyclerView.Adapter() { + var listKeys = getKeys() + open fun getKeys(): List? = null + open fun getValue(key: String): String? = null + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { + return ListViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.line_word, parent, false) ) } @SuppressLint("ClickableViewAccessibility", "SetTextI18n") - override fun onBindViewHolder(holder: ViewData, position: Int) { - Log.d("MyMain", "Bind $position") - if(position < keys.size) { - holder.itemView.ta.text = keys[position] - if(position < datas.size) holder.itemView.tb.text = datas[position] - holder.itemView.setOnClickListener { - showDictAlert(keys[position], if(position < datas.size) datas[position] else "null") + override fun onBindViewHolder(holder: ListViewHolder, position: Int) { + Log.d("MyMain", "Bind like at $position") + listKeys?.apply { + if (position < size) { + val key = get(position) + val data = getValue(key) + holder.itemView.apply { + ta.text = key + tb.text = data + setOnClickListener { + showDictAlert(key, data) + } + } } } } - override fun getItemCount() = count + override fun getItemCount() = listKeys?.size?:0 - fun filter(text: CharSequence) { - dict.pattern = text - dict.keys.let { - count = it.size - if (count > 0) { - keys = arrayOf() - datas = arrayOf() - it.forEach { - keys += it - datas += dict[it] - Log.d("MyMain", "Get key: $it is ${datas.last()}") - } - } - } + fun refresh() { + listKeys = getKeys() runOnUiThread { notifyDataSetChanged() } } } diff --git a/app/src/main/java/top/fumiama/simpledict/SimpleDict.kt b/app/src/main/java/top/fumiama/simpledict/SimpleDict.kt index 741dd4f..ab4f090 100644 --- a/app/src/main/java/top/fumiama/simpledict/SimpleDict.kt +++ b/app/src/main/java/top/fumiama/simpledict/SimpleDict.kt @@ -1,46 +1,97 @@ package top.fumiama.simpledict +import android.util.Log import java.lang.Thread.sleep -class SimpleDict(private val client: Client): HashMap() { //must run in thread - var pattern: CharSequence = "a" - private var isInit = true - override val keys: MutableSet +class SimpleDict(private val client: Client, private val pwd: String) { //must run in thread + private var dict = HashMap() + val keys get() = dict.keys + val values get() = dict.values + //val size get() = dict.size + private val raw: ByteArray? get() { - val re = mutableSetOf() - if(isInit) { - isInit = false - return re - } else { - client.initConnect() - client.sendMessage("lst") - sleep(233) - client.receiveMessage() - client.sendMessage(pattern) - client.receiveMessage()?.substringBeforeLast('\n')?.split('\n')?.forEach { - re.add(it) - } - client.sendMessage("quit") - client.closeConnect() - return re - } + initDict() + client.sendMessage("cat") + sleep(233) + val re = client.receiveRawMessage() + closeDict() + return re } - override fun get(key: String): String? { + init { + Thread{ fetchDict() }.start() + } + + private fun initDict() { client.initConnect() + client.sendMessage(pwd) + } + + private fun closeDict() { + client.sendMessage("quit") + client.closeConnect() + } + + private fun analyzeDictBlk(dictBlock: ByteArray) { + Log.d("MySD", "Read block: ${dictBlock.decodeToString()}") + val keyLen = dictBlock[63].toInt().let { if (it > 63) 63 else it } + val dataEnd = 64 + dictBlock[127].toInt().let { if (it > 63) 63 else it } + val key = dictBlock.copyOf(keyLen).decodeToString() + val data = if (dataEnd > 64) dictBlock.copyOfRange(64, dataEnd).decodeToString() else null + dict[key] = data + Log.d("MySD", "Fetch $key=$data") + } + + //fun filterKeys(predicate: (String) -> Boolean) = dict.filterKeys(predicate) + fun filterValues(predicate: (String?) -> Boolean) = dict.filterValues(predicate) + + fun fetchDict() { + val dictBlock = ByteArray(128) + raw?.inputStream()?.let { + var c = '1' + while (!it.read().toChar().isDigit()) Log.d("MySD", "Skip banner.") + while (c.isDigit()) { + c = it.read().toChar() + Log.d("MySD", "Skip digit $c.") + } + dictBlock[0] = c.toByte() + if(it.read(dictBlock, 1, 127) == 127) { + analyzeDictBlk(dictBlock) + while (it.read(dictBlock, 0, 128) == 128) analyzeDictBlk(dictBlock) + } + } + } + + /*fun keysWithPattern(pattern: String): MutableSet{ + val re = mutableSetOf() + initDict() + client.sendMessage("lst") + sleep(233) + client.receiveMessage() + client.sendMessage(pattern) + client.receiveMessage()?.substringBeforeLast('\n')?.split('\n')?.forEach { + re.add(it) + } + closeDict() + return re + } + + fun getDirectly(key: String): String? { + initDict() client.sendMessage("get") sleep(233) client.receiveMessage() client.sendMessage(key) val re = client.receiveMessage() - client.sendMessage("quit") - client.closeConnect() + closeDict() return re - } + }*/ - override fun put(key: String, value: String): String? { - val p = this[key] - client.initConnect() + operator fun get(key: String) = dict[key] + + operator fun set(key: String, value: String): String? { + val p = dict[key] + initDict() client.sendMessage("set") sleep(233) client.receiveMessage() @@ -48,8 +99,7 @@ class SimpleDict(private val client: Client): HashMap() { //mu client.receiveMessage() client.sendMessage(value) client.receiveMessage() - client.sendMessage("quit") - client.closeConnect() + closeDict() return p } } \ No newline at end of file diff --git a/app/src/main/res/font/gotham.ttf b/app/src/main/res/font/gotham.ttf new file mode 100644 index 0000000..58018ad Binary files /dev/null and b/app/src/main/res/font/gotham.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9d773cd..88831e1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,50 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + + + android:layout_width="match_parent" + android:layout_height="match_parent"> + + + android:layout_height="wrap_content" + android:background="?attr/colorSurface"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/line_word.xml b/app/src/main/res/layout/line_word.xml index 8025e1c..de6b47b 100644 --- a/app/src/main/res/layout/line_word.xml +++ b/app/src/main/res/layout/line_word.xml @@ -15,9 +15,10 @@ android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" + android:fontFamily="@font/gotham" android:text="TextView" - android:textAppearance="@style/TextAppearance.AppCompat.Body2" - android:textSize="18sp" /> + android:textColor="?attr/colorOnSurface" + android:textSize="22sp" />