Merge remote-tracking branch 'origin/master'

This commit is contained in:
kunfei
2023-11-29 11:27:44 +08:00
15 changed files with 189 additions and 61 deletions
+2 -2
View File
@@ -250,8 +250,8 @@
## 对外提供api ## 对外提供api
-keep class io.legado.app.api.ReturnData{*;} -keep class io.legado.app.api.ReturnData{*;}
# Apache Commons Compress # 繁简转换
-keep class org.apache.commons.compress.archivers.** {*;} -keep class com.github.liuyueyi.quick.transfer.** {*;}
#-------------------Cronet------------------------------------ #-------------------Cronet------------------------------------
+5 -2
View File
@@ -7,7 +7,6 @@ import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import com.github.liuyueyi.quick.transfer.ChineseUtils
import com.github.liuyueyi.quick.transfer.constants.TransType import com.github.liuyueyi.quick.transfer.constants.TransType
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import io.legado.app.base.AppContextWrapper import io.legado.app.base.AppContextWrapper
@@ -31,6 +30,7 @@ import io.legado.app.help.http.okHttpClient
import io.legado.app.help.source.SourceHelp import io.legado.app.help.source.SourceHelp
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.model.BookCover import io.legado.app.model.BookCover
import io.legado.app.utils.ChineseUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -73,7 +73,10 @@ class App : Application() {
Backup.clearCache() Backup.clearCache()
//初始化简繁转换引擎 //初始化简繁转换引擎
when (AppConfig.chineseConverterType) { when (AppConfig.chineseConverterType) {
1 -> ChineseUtils.preLoad(true, TransType.TRADITIONAL_TO_SIMPLE) 1 -> launch {
ChineseUtils.fixT2sDict()
}
2 -> ChineseUtils.preLoad(true, TransType.SIMPLE_TO_TRADITIONAL) 2 -> ChineseUtils.preLoad(true, TransType.SIMPLE_TO_TRADITIONAL)
} }
//调整排序序号 //调整排序序号
@@ -5,7 +5,6 @@ import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
@@ -4,8 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Looper
import android.webkit.WebSettings import android.webkit.WebSettings
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
@@ -37,9 +39,26 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
* uncaughtException 回调函数 * uncaughtException 回调函数
*/ */
override fun uncaughtException(thread: Thread, ex: Throwable) { override fun uncaughtException(thread: Thread, ex: Throwable) {
ReadAloud.stop(context) if (shouldAbsorb(ex)) {
handleException(ex) AppLog.put("发生未捕获的异常\n${ex.localizedMessage}", ex)
mDefaultHandler?.uncaughtException(thread, ex) Looper.loop()
} else {
ReadAloud.stop(context)
handleException(ex)
mDefaultHandler?.uncaughtException(thread, ex)
}
}
private fun shouldAbsorb(e: Throwable): Boolean {
return when {
e::class.simpleName == "CannotDeliverBroadcastException" -> true
e is SecurityException && e.message?.contains(
"nor current process has android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS",
true
) == true -> true
else -> false
}
} }
/** /**
@@ -107,7 +126,8 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
val time = format.format(Date()) val time = format.format(Date())
val fileName = "crash-$time-$timestamp.log" val fileName = "crash-$time-$timestamp.log"
try { try {
val backupPath = AppConfig.backupPath ?: throw NoStackTraceException("备份路径未配置") val backupPath = AppConfig.backupPath
?: throw NoStackTraceException("备份路径未配置")
val uri = Uri.parse(backupPath) val uri = Uri.parse(backupPath)
val fileDoc = FileDoc.fromUri(uri, true) val fileDoc = FileDoc.fromUri(uri, true)
fileDoc.createFileIfNotExist(fileName, "crash") fileDoc.createFileIfNotExist(fileName, "crash")
@@ -5,7 +5,7 @@ import android.webkit.WebSettings
import androidx.annotation.Keep import androidx.annotation.Keep
import cn.hutool.core.codec.Base64 import cn.hutool.core.codec.Base64
import cn.hutool.core.util.HexUtil import cn.hutool.core.util.HexUtil
import com.github.liuyueyi.quick.transfer.ChineseUtils import io.legado.app.utils.ChineseUtils
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
@@ -1,6 +1,5 @@
package io.legado.app.help.book package io.legado.app.help.book
import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern.spaceRegex import io.legado.app.constant.AppPattern.spaceRegex
import io.legado.app.data.appDb import io.legado.app.data.appDb
@@ -10,6 +9,7 @@ import io.legado.app.data.entities.ReplaceRule
import io.legado.app.exception.RegexTimeoutException import io.legado.app.exception.RegexTimeoutException
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.utils.ChineseUtils
import io.legado.app.utils.escapeRegex import io.legado.app.utils.escapeRegex
import io.legado.app.utils.replace import io.legado.app.utils.replace
import io.legado.app.utils.stackTraceStr import io.legado.app.utils.stackTraceStr
@@ -41,6 +41,7 @@ import io.legado.app.utils.activityPendingIntent
import io.legado.app.utils.cnCompare import io.legado.app.utils.cnCompare
import io.legado.app.utils.createFolderIfNotExist import io.legado.app.utils.createFolderIfNotExist
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.outputStream
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.readBytes import io.legado.app.utils.readBytes
import io.legado.app.utils.readText import io.legado.app.utils.readText
@@ -140,7 +141,7 @@ class ExportBookService : BaseService() {
startForeground(NotificationId.ExportBookService, notification.build()) startForeground(NotificationId.ExportBookService, notification.build())
} }
private fun upExportNotification() { private fun upExportNotification(finish: Boolean = false) {
val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload) val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_export) .setSmallIcon(R.drawable.ic_export)
.setSubText(getString(R.string.export_book)) .setSubText(getString(R.string.export_book))
@@ -149,7 +150,7 @@ class ExportBookService : BaseService() {
.setContentText(notificationContentText) .setContentText(notificationContentText)
.setDeleteIntent(servicePendingIntent<ExportBookService>(IntentAction.stop)) .setDeleteIntent(servicePendingIntent<ExportBookService>(IntentAction.stop))
.setGroup(groupKey) .setGroup(groupKey)
if (exportJob?.isActive == true) { if (!finish) {
notification.setOngoing(true) notification.setOngoing(true)
notification.addAction( notification.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
@@ -168,7 +169,7 @@ class ExportBookService : BaseService() {
while (true) { while (true) {
val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let { val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let {
notificationContentText = "导出完成" notificationContentText = "导出完成"
upExportNotification() upExportNotification(true)
return@launch return@launch
} }
exportProgress[bookUrl] = 0 exportProgress[bookUrl] = 0
@@ -230,21 +231,24 @@ class ExportBookService : BaseService() {
DocumentUtils.delete(doc, filename) DocumentUtils.delete(doc, filename)
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename) val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹") ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
val charset = Charset.forName(AppConfig.exportCharset)
contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
getAllContents(book) { text, srcList -> BufferedOutputStream(bookOs, 64 * 1024).use { bos ->
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset))) getAllContents(book) { text, srcList ->
srcList?.forEach { bos.write(text.toByteArray(charset))
val vFile = BookHelp.getImage(book, it.src) srcList?.forEach {
if (vFile.exists()) { val vFile = BookHelp.getImage(book, it.src)
DocumentUtils.createFileIfNotExist( if (vFile.exists()) {
doc, DocumentUtils.createFileIfNotExist(
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg", doc,
subDirs = arrayOf( "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
"${book.name}_${book.author}", subDirs = arrayOf(
"images", "${book.name}_${book.author}",
it.chapterTitle "images",
) it.chapterTitle
)?.writeBytes(this, vFile.readBytes()) )
)?.writeBytes(this, vFile.readBytes())
}
} }
} }
} }
@@ -259,18 +263,22 @@ class ExportBookService : BaseService() {
val filename = book.getExportFileName("txt") val filename = book.getExportFileName("txt")
val bookPath = FileUtils.getPath(file, filename) val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath) val bookFile = FileUtils.createFileWithReplace(bookPath)
getAllContents(book) { text, srcList -> val charset = Charset.forName(AppConfig.exportCharset)
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset)) val bos = BufferedOutputStream(bookFile.outputStream(true), 64 * 1024)
srcList?.forEach { bos.use {
val vFile = BookHelp.getImage(book, it.src) getAllContents(book) { text, srcList ->
if (vFile.exists()) { bos.write(text.toByteArray(charset))
FileUtils.createFileIfNotExist( srcList?.forEach {
file, val vFile = BookHelp.getImage(book, it.src)
"${book.name}_${book.author}", if (vFile.exists()) {
"images", FileUtils.createFileIfNotExist(
it.chapterTitle, file,
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg" "${book.name}_${book.author}",
).writeBytes(vFile.readBytes()) "images",
it.chapterTitle,
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg"
).writeBytes(vFile.readBytes())
}
} }
} }
} }
@@ -249,10 +249,11 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
val fontPath = ReadBookConfig.textFont val fontPath = ReadBookConfig.textFont
if (fontPath.isNotEmpty()) { if (fontPath.isNotEmpty()) {
val fontName = FileUtils.getName(fontPath) val fontName = FileUtils.getName(fontPath)
val fontBytes = fontPath.parseToUri().readBytes(requireContext()) val fontInputStream =
fontBytes.let { fontPath.parseToUri().inputStream(requireContext()).getOrNull()
fontInputStream?.use {
val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName) val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
fontExportFile.writeBytes(it) it.copyTo(fontExportFile.outputStream())
exportFiles.add(fontExportFile) exportFiles.add(fontExportFile)
} }
} }
@@ -261,8 +262,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
val bgFile = File(ReadBookConfig.durConfig.bgStr) val bgFile = File(ReadBookConfig.durConfig.bgStr)
if (bgFile.exists()) { if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName)) val bgExportFile = File(FileUtils.getPath(configDir, bgName))
bgFile.copyTo(bgExportFile) if (!bgExportFile.exists()) {
exportFiles.add(bgExportFile) bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
} }
} }
if (ReadBookConfig.durConfig.bgTypeNight == 2) { if (ReadBookConfig.durConfig.bgTypeNight == 2) {
@@ -270,8 +273,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
val bgFile = File(ReadBookConfig.durConfig.bgStrNight) val bgFile = File(ReadBookConfig.durConfig.bgStrNight)
if (bgFile.exists()) { if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName)) val bgExportFile = File(FileUtils.getPath(configDir, bgName))
bgFile.copyTo(bgExportFile) if (!bgExportFile.exists()) {
exportFiles.add(bgExportFile) bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
} }
} }
if (ReadBookConfig.durConfig.bgTypeEInk == 2) { if (ReadBookConfig.durConfig.bgTypeEInk == 2) {
@@ -279,8 +284,10 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
val bgFile = File(ReadBookConfig.durConfig.bgStrEInk) val bgFile = File(ReadBookConfig.durConfig.bgStrEInk)
if (bgFile.exists()) { if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName)) val bgExportFile = File(FileUtils.getPath(configDir, bgName))
bgFile.copyTo(bgExportFile) if (!bgExportFile.exists()) {
exportFiles.add(bgExportFile) bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
} }
} }
val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName) val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
@@ -288,14 +295,17 @@ class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) {
if (uri.isContentScheme()) { if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc -> DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc ->
treeDoc.findFile(exportFileName)?.delete() treeDoc.findFile(exportFileName)?.delete()
treeDoc.createFile("", exportFileName) val out = treeDoc.createFile("", exportFileName)?.openOutputStream()
?.writeBytes(requireContext(), File(configZipPath).readBytes()) out?.use {
File(configZipPath).inputStream().use {
it.copyTo(out)
}
}
} }
} else { } else {
val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName) val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName)
FileUtils.delete(exportPath) FileUtils.delete(exportPath)
FileUtils.createFileIfNotExist(exportPath) File(configZipPath).copyTo(FileUtils.createFileIfNotExist(exportPath))
.writeBytes(File(configZipPath).readBytes())
} }
} }
}.onSuccess { }.onSuccess {
@@ -7,7 +7,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import androidx.core.view.get import androidx.core.view.get
import com.github.liuyueyi.quick.transfer.ChineseUtils
import com.github.liuyueyi.quick.transfer.constants.TransType import com.github.liuyueyi.quick.transfer.constants.TransType
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
@@ -2,7 +2,6 @@ package io.legado.app.ui.book.searchContent
import android.app.Application import android.app.Application
import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@@ -10,13 +9,14 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.utils.ChineseUtils
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
class SearchContentViewModel(application: Application) : BaseViewModel(application) { class SearchContentViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = "" var bookUrl: String = ""
var book: Book? = null var book: Book? = null
var contentProcessor: ContentProcessor? = null private var contentProcessor: ContentProcessor? = null
var lastQuery: String = "" var lastQuery: String = ""
var searchResultCounts = 0 var searchResultCounts = 0
val cacheChapterNames = hashSetOf<String>() val cacheChapterNames = hashSetOf<String>()
@@ -246,7 +246,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
} }
else -> { else -> {
binding.root.longSnackbar("跳转其它应用", "确认") { binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) {
openUrl(url) openUrl(url)
} }
return true return true
@@ -0,0 +1,45 @@
package io.legado.app.utils
import com.github.liuyueyi.quick.transfer.ChineseUtils
import com.github.liuyueyi.quick.transfer.constants.TransType
import com.github.liuyueyi.quick.transfer.dictionary.DictionaryContainer
object ChineseUtils {
private var fixed = false
fun s2t(content: String): String {
return ChineseUtils.s2t(content)
}
fun t2s(content: String): String {
if (!fixed) {
fixed = true
fixT2sDict()
}
return ChineseUtils.t2s(content)
}
fun preLoad(async: Boolean, vararg transType: TransType) {
ChineseUtils.preLoad(async, *transType)
}
fun unLoad(vararg transType: TransType) {
ChineseUtils.unLoad(*transType)
}
fun fixT2sDict() {
val dict = DictionaryContainer.getInstance().getDictionary(TransType.TRADITIONAL_TO_SIMPLE)
dict.chars.run {
remove('劈')
remove('脊')
}
kotlin.runCatching {
dict.dict.run {
remove("支援")
remove("路易斯")
}
}
}
}
@@ -212,7 +212,7 @@ fun FileDoc.createFileIfNotExist(
vararg subDirs: String vararg subDirs: String
): FileDoc { ): FileDoc {
return if (uri.isContentScheme()) { return if (uri.isContentScheme()) {
val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!! val documentFile = asDocumentFile()!!
val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *subDirs)!! val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *subDirs)!!
FileDoc.fromDocumentFile(tmp) FileDoc.fromDocumentFile(tmp)
} else { } else {
@@ -226,7 +226,7 @@ fun FileDoc.createFolderIfNotExist(
vararg subDirs: String vararg subDirs: String
): FileDoc { ): FileDoc {
return if (uri.isContentScheme()) { return if (uri.isContentScheme()) {
val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!! val documentFile = asDocumentFile()!!
val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!! val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!!
FileDoc.fromDocumentFile(tmp) FileDoc.fromDocumentFile(tmp)
} else { } else {
@@ -257,7 +257,7 @@ fun FileDoc.exists(
vararg subDirs: String vararg subDirs: String
): Boolean { ): Boolean {
return if (uri.isContentScheme()) { return if (uri.isContentScheme()) {
DocumentUtils.exists(DocumentFile.fromTreeUri(appCtx, uri)!!, fileName, *subDirs) DocumentUtils.exists(asDocumentFile()!!, fileName, *subDirs)
} else { } else {
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
FileUtils.exist(path) FileUtils.exist(path)
@@ -266,7 +266,7 @@ fun FileDoc.exists(
fun FileDoc.exists(): Boolean { fun FileDoc.exists(): Boolean {
return if (uri.isContentScheme()) { return if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(appCtx, uri)!!.exists() asDocumentFile()!!.exists()
} else { } else {
FileUtils.exist(uri.path!!) FileUtils.exist(uri.path!!)
} }
@@ -4,6 +4,7 @@ package io.legado.app.utils
import android.net.Uri import android.net.Uri
import java.io.File import java.io.File
import java.io.FileOutputStream
fun File.getFile(vararg subDirFiles: String): File { fun File.getFile(vararg subDirFiles: String): File {
val path = FileUtils.getPath(this, *subDirFiles) val path = FileUtils.getPath(this, *subDirFiles)
@@ -79,3 +80,7 @@ fun File.checkWrite(): Boolean {
false false
} }
} }
fun File.outputStream(append: Boolean = false): FileOutputStream {
return FileOutputStream(this, append)
}
@@ -0,0 +1,39 @@
package io.legado.app.utils
import com.github.liuyueyi.quick.transfer.Trie
import com.github.liuyueyi.quick.transfer.TrieNode
import java.util.HashMap
fun <T> Trie<T>.getRoot(): TrieNode<T> {
val rootField = javaClass.getDeclaredField("root")
.apply { isAccessible = true }
@Suppress("UNCHECKED_CAST")
return rootField.get(this) as TrieNode<T>
}
fun <T> TrieNode<T>.getChildren(): HashMap<Char, TrieNode<T>> {
val childrenField = javaClass.getDeclaredField("children")
.apply { isAccessible = true }
@Suppress("UNCHECKED_CAST")
return childrenField.get(this) as HashMap<Char, TrieNode<T>>
}
fun <T> Trie<T>.remove(value: String) {
var node = getRoot()
val nodes = arrayListOf<TrieNode<T>>()
val chars = value.toCharArray()
for (c in chars) {
nodes.add(node)
node = node.getChildren()[c] ?: break
if (!node.isLeaf) {
continue
}
for ((ch, n) in chars.reversed().zip(nodes.reversed())) {
val children = n.getChildren()
children.remove(ch)
if (children.isNotEmpty()) {
break
}
}
}
}