mirror of
https://github.com/fumiama/simple-dict-android.git
synced 2026-06-05 08:40:25 +08:00
v1.0
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
|
||||
@@ -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<String>()
|
||||
private var datas = arrayOf<String?>()
|
||||
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<ViewData>() {
|
||||
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<String>()
|
||||
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<ListViewHolder>() {
|
||||
var listKeys = getKeys()
|
||||
open fun getKeys(): List<String>? = 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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,97 @@
|
||||
package top.fumiama.simpledict
|
||||
|
||||
import android.util.Log
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
class SimpleDict(private val client: Client): HashMap<String, String>() { //must run in thread
|
||||
var pattern: CharSequence = "a"
|
||||
private var isInit = true
|
||||
override val keys: MutableSet<String>
|
||||
class SimpleDict(private val client: Client, private val pwd: String) { //must run in thread
|
||||
private var dict = HashMap<String, String?>()
|
||||
val keys get() = dict.keys
|
||||
val values get() = dict.values
|
||||
//val size get() = dict.size
|
||||
private val raw: ByteArray?
|
||||
get() {
|
||||
val re = mutableSetOf<String>()
|
||||
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<String>{
|
||||
val re = mutableSetOf<String>()
|
||||
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<String, String>() { //mu
|
||||
client.receiveMessage()
|
||||
client.sendMessage(value)
|
||||
client.receiveMessage()
|
||||
client.sendMessage("quit")
|
||||
client.closeConnect()
|
||||
closeDict()
|
||||
return p
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/font/gotham.ttf
Normal file
BIN
app/src/main/res/font/gotham.ttf
Normal file
Binary file not shown.
@@ -1,13 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
|
||||
<com.lapism.search.widget.MaterialSearchView
|
||||
android:id="@+id/ffms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:contentScrim="?attr/colorSurface"
|
||||
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<com.lapism.search.widget.MaterialSearchView
|
||||
android:id="@+id/ffms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="com.lapism.search.widget.SearchBehavior" />
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/ffsw"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/ffns"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/ffr"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tb"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user