commit
						0aa6ee9803
					
				@ -0,0 +1,62 @@ | 
				
			|||||||
 | 
					[ | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029304, | 
				
			||||||
 | 
					    "name": "度小美", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029305, | 
				
			||||||
 | 
					    "name": "度小宇", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029306, | 
				
			||||||
 | 
					    "name": "度逍遥", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029307, | 
				
			||||||
 | 
					    "name": "度丫丫", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029308, | 
				
			||||||
 | 
					    "name": "度小娇", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029309, | 
				
			||||||
 | 
					    "name": "度米朵", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029310, | 
				
			||||||
 | 
					    "name": "度博文", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029311, | 
				
			||||||
 | 
					    "name": "度小童", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029312, | 
				
			||||||
 | 
					    "name": "度小萌", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029313, | 
				
			||||||
 | 
					    "name": "百度骚男", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029314, | 
				
			||||||
 | 
					    "name": "百度评书", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  }, | 
				
			||||||
 | 
					  { | 
				
			||||||
 | 
					    "id": 1598233029315, | 
				
			||||||
 | 
					    "name": "百度主持", | 
				
			||||||
 | 
					    "url": "http://tts.baidu.com/text2audio,{\n    \"method\": \"POST\",\n    \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					] | 
				
			||||||
@ -0,0 +1,31 @@ | 
				
			|||||||
 | 
					package io.legado.app.data.dao | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.lifecycle.LiveData | 
				
			||||||
 | 
					import androidx.room.* | 
				
			||||||
 | 
					import io.legado.app.data.entities.HttpTTS | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Dao | 
				
			||||||
 | 
					interface HttpTTSDao { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @get:Query("select * from httpTTS order by name") | 
				
			||||||
 | 
					    val all: List<HttpTTS> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Query("select * from httpTTS order by name") | 
				
			||||||
 | 
					    fun observeAll(): LiveData<List<HttpTTS>> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @get:Query("select count(*) from httpTTS") | 
				
			||||||
 | 
					    val count: Int | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Query("select * from httpTTS where id = :id") | 
				
			||||||
 | 
					    fun get(id: Long): HttpTTS? | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Insert(onConflict = OnConflictStrategy.REPLACE) | 
				
			||||||
 | 
					    fun insert(vararg httpTTS: HttpTTS) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Delete | 
				
			||||||
 | 
					    fun delete(vararg httpTTS: HttpTTS) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Update | 
				
			||||||
 | 
					    fun update(vararg httpTTS: HttpTTS) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,12 @@ | 
				
			|||||||
 | 
					package io.legado.app.data.entities | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.room.Entity | 
				
			||||||
 | 
					import androidx.room.PrimaryKey | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity(tableName = "httpTTS") | 
				
			||||||
 | 
					data class HttpTTS( | 
				
			||||||
 | 
					    @PrimaryKey | 
				
			||||||
 | 
					    val id: Long = System.currentTimeMillis(), | 
				
			||||||
 | 
					    var name: String = "", | 
				
			||||||
 | 
					    var url: String = "" | 
				
			||||||
 | 
					) | 
				
			||||||
@ -1,11 +1,10 @@ | 
				
			|||||||
package io.legado.app.data.entities | 
					package io.legado.app.data.entities | 
				
			||||||
 | 
					
 | 
				
			||||||
import androidx.room.Entity | 
					import androidx.room.Entity | 
				
			||||||
import androidx.room.PrimaryKey | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity(tableName = "readRecord") | 
					@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"]) | 
				
			||||||
data class ReadRecord( | 
					data class ReadRecord( | 
				
			||||||
    @PrimaryKey | 
					    var androidId: String = "", | 
				
			||||||
    var bookName: String = "", | 
					    var bookName: String = "", | 
				
			||||||
    var readTime: Long = 0L | 
					    var readTime: Long = 0L | 
				
			||||||
) | 
					) | 
				
			||||||
@ -0,0 +1,6 @@ | 
				
			|||||||
 | 
					package io.legado.app.data.entities | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class ReadRecordShow( | 
				
			||||||
 | 
					    var bookName: String = "", | 
				
			||||||
 | 
					    var readTime: Long = 0L | 
				
			||||||
 | 
					) | 
				
			||||||
@ -0,0 +1,19 @@ | 
				
			|||||||
 | 
					package io.legado.app.help | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import io.legado.app.App | 
				
			||||||
 | 
					import io.legado.app.data.entities.HttpTTS | 
				
			||||||
 | 
					import io.legado.app.utils.GSON | 
				
			||||||
 | 
					import io.legado.app.utils.fromJsonArray | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object DefaultValueHelp { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun initHttpTTS() { | 
				
			||||||
 | 
					        val json = String(App.INSTANCE.assets.open("httpTTS.json").readBytes()) | 
				
			||||||
 | 
					        GSON.fromJsonArray<HttpTTS>(json)?.let { | 
				
			||||||
 | 
					            App.db.httpTTSDao().insert(*it.toTypedArray()) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -1,93 +0,0 @@ | 
				
			|||||||
package io.legado.app.model | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import io.legado.app.App | 
					 | 
				
			||||||
import io.legado.app.data.entities.SearchBook | 
					 | 
				
			||||||
import io.legado.app.help.AppConfig | 
					 | 
				
			||||||
import io.legado.app.help.coroutine.Coroutine | 
					 | 
				
			||||||
import io.legado.app.utils.getPrefString | 
					 | 
				
			||||||
import kotlinx.coroutines.CoroutineScope | 
					 | 
				
			||||||
import kotlinx.coroutines.Dispatchers.IO | 
					 | 
				
			||||||
import kotlinx.coroutines.asCoroutineDispatcher | 
					 | 
				
			||||||
import java.util.concurrent.Executors | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) { | 
					 | 
				
			||||||
    private var searchPool = | 
					 | 
				
			||||||
        Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() | 
					 | 
				
			||||||
    private var mSearchId = System.currentTimeMillis() | 
					 | 
				
			||||||
    private var searchPage = 1 | 
					 | 
				
			||||||
    private var searchKey: String = "" | 
					 | 
				
			||||||
    private var task: Coroutine<*>? = null | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun initSearchPool() { | 
					 | 
				
			||||||
        searchPool = | 
					 | 
				
			||||||
            Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fun search(searchId: Long, key: String) { | 
					 | 
				
			||||||
        if (searchId != mSearchId) { | 
					 | 
				
			||||||
            task?.cancel() | 
					 | 
				
			||||||
            searchPool.close() | 
					 | 
				
			||||||
            initSearchPool() | 
					 | 
				
			||||||
            mSearchId = searchId | 
					 | 
				
			||||||
            searchPage = 1 | 
					 | 
				
			||||||
            if (key.isEmpty()) { | 
					 | 
				
			||||||
                return | 
					 | 
				
			||||||
            } else { | 
					 | 
				
			||||||
                this.searchKey = key | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
        } else { | 
					 | 
				
			||||||
            searchPage++ | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
        task = Coroutine.async(scope, searchPool) { | 
					 | 
				
			||||||
            val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: "" | 
					 | 
				
			||||||
            val bookSourceList = if (searchGroup.isBlank()) { | 
					 | 
				
			||||||
                App.db.bookSourceDao().allEnabled | 
					 | 
				
			||||||
            } else { | 
					 | 
				
			||||||
                App.db.bookSourceDao().getEnabledByGroup(searchGroup) | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
            for (item in bookSourceList) { | 
					 | 
				
			||||||
                //task取消时自动取消 by (scope = this@execute) | 
					 | 
				
			||||||
                WebBook(item).searchBook( | 
					 | 
				
			||||||
                        searchKey, | 
					 | 
				
			||||||
                        searchPage, | 
					 | 
				
			||||||
                        scope = this, | 
					 | 
				
			||||||
                        context = searchPool | 
					 | 
				
			||||||
                    ) | 
					 | 
				
			||||||
                    .timeout(30000L) | 
					 | 
				
			||||||
                    .onSuccess(IO) { | 
					 | 
				
			||||||
                        if (searchId == mSearchId) { | 
					 | 
				
			||||||
                            callBack.onSearchSuccess(it) | 
					 | 
				
			||||||
                        } | 
					 | 
				
			||||||
                    } | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
        }.onStart { | 
					 | 
				
			||||||
            callBack.onSearchStart() | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        task?.invokeOnCompletion { | 
					 | 
				
			||||||
            if (searchId == mSearchId) { | 
					 | 
				
			||||||
                callBack.onSearchFinish() | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fun cancelSearch() { | 
					 | 
				
			||||||
        task?.cancel() | 
					 | 
				
			||||||
        mSearchId = 0 | 
					 | 
				
			||||||
        callBack.onSearchCancel() | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fun close() { | 
					 | 
				
			||||||
        task?.cancel() | 
					 | 
				
			||||||
        mSearchId = 0 | 
					 | 
				
			||||||
        searchPool.close() | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    interface CallBack { | 
					 | 
				
			||||||
        fun onSearchStart() | 
					 | 
				
			||||||
        fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) | 
					 | 
				
			||||||
        fun onSearchFinish() | 
					 | 
				
			||||||
        fun onSearchCancel() | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
@ -1,4 +1,4 @@ | 
				
			|||||||
package io.legado.app.model | 
					package io.legado.app.model.rss | 
				
			||||||
 | 
					
 | 
				
			||||||
import io.legado.app.data.entities.RssArticle | 
					import io.legado.app.data.entities.RssArticle | 
				
			||||||
import io.legado.app.data.entities.RssSource | 
					import io.legado.app.data.entities.RssSource | 
				
			||||||
@ -0,0 +1,117 @@ | 
				
			|||||||
 | 
					package io.legado.app.model.webBook | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import io.legado.app.App | 
				
			||||||
 | 
					import io.legado.app.data.entities.BookSource | 
				
			||||||
 | 
					import io.legado.app.data.entities.SearchBook | 
				
			||||||
 | 
					import io.legado.app.help.AppConfig | 
				
			||||||
 | 
					import io.legado.app.help.coroutine.CompositeCoroutine | 
				
			||||||
 | 
					import io.legado.app.utils.getPrefString | 
				
			||||||
 | 
					import kotlinx.coroutines.CoroutineScope | 
				
			||||||
 | 
					import kotlinx.coroutines.Dispatchers.IO | 
				
			||||||
 | 
					import kotlinx.coroutines.ExecutorCoroutineDispatcher | 
				
			||||||
 | 
					import kotlinx.coroutines.asCoroutineDispatcher | 
				
			||||||
 | 
					import java.util.concurrent.Executors | 
				
			||||||
 | 
					import kotlin.math.min | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) { | 
				
			||||||
 | 
					    val threadCount = AppConfig.threadCount | 
				
			||||||
 | 
					    private var searchPool: ExecutorCoroutineDispatcher? = null | 
				
			||||||
 | 
					    private var mSearchId = 0L | 
				
			||||||
 | 
					    private var searchPage = 1 | 
				
			||||||
 | 
					    private var searchKey: String = "" | 
				
			||||||
 | 
					    private var tasks = CompositeCoroutine() | 
				
			||||||
 | 
					    private var bookSourceList = arrayListOf<BookSource>() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Volatile | 
				
			||||||
 | 
					    private var searchIndex = -1 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun initSearchPool() { | 
				
			||||||
 | 
					        searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun search(searchId: Long, key: String) { | 
				
			||||||
 | 
					        callBack.onSearchStart() | 
				
			||||||
 | 
					        if (searchId != mSearchId) { | 
				
			||||||
 | 
					            if (key.isEmpty()) { | 
				
			||||||
 | 
					                callBack.onSearchCancel() | 
				
			||||||
 | 
					                return | 
				
			||||||
 | 
					            } else { | 
				
			||||||
 | 
					                this.searchKey = key | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            if (mSearchId != 0L) { | 
				
			||||||
 | 
					                close() | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            initSearchPool() | 
				
			||||||
 | 
					            mSearchId = searchId | 
				
			||||||
 | 
					            searchPage = 1 | 
				
			||||||
 | 
					            val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: "" | 
				
			||||||
 | 
					            bookSourceList.clear() | 
				
			||||||
 | 
					            if (searchGroup.isBlank()) { | 
				
			||||||
 | 
					                bookSourceList.addAll(App.db.bookSourceDao().allEnabled) | 
				
			||||||
 | 
					            } else { | 
				
			||||||
 | 
					                bookSourceList.addAll(App.db.bookSourceDao().getEnabledByGroup(searchGroup)) | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					            searchPage++ | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        searchIndex = -1 | 
				
			||||||
 | 
					        for (i in 0 until threadCount) { | 
				
			||||||
 | 
					            search(searchId) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun search(searchId: Long) { | 
				
			||||||
 | 
					        synchronized(this) { | 
				
			||||||
 | 
					            if (searchIndex >= bookSourceList.lastIndex) { | 
				
			||||||
 | 
					                return | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            searchIndex++ | 
				
			||||||
 | 
					            val source = bookSourceList[searchIndex] | 
				
			||||||
 | 
					            val task = WebBook(source).searchBook( | 
				
			||||||
 | 
					                searchKey, | 
				
			||||||
 | 
					                searchPage, | 
				
			||||||
 | 
					                scope = scope, | 
				
			||||||
 | 
					                context = searchPool!! | 
				
			||||||
 | 
					            ).timeout(30000L) | 
				
			||||||
 | 
					                .onSuccess(IO) { | 
				
			||||||
 | 
					                    if (searchId == mSearchId) { | 
				
			||||||
 | 
					                        callBack.onSearchSuccess(it) | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                .onFinally { | 
				
			||||||
 | 
					                    synchronized(this) { | 
				
			||||||
 | 
					                        if (searchIndex < bookSourceList.lastIndex) { | 
				
			||||||
 | 
					                            search(searchId) | 
				
			||||||
 | 
					                        } else { | 
				
			||||||
 | 
					                            searchIndex++ | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                        if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, | 
				
			||||||
 | 
					                                threadCount) | 
				
			||||||
 | 
					                        ) { | 
				
			||||||
 | 
					                            callBack.onSearchFinish() | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            tasks.add(task) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun cancelSearch() { | 
				
			||||||
 | 
					        close() | 
				
			||||||
 | 
					        callBack.onSearchCancel() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun close() { | 
				
			||||||
 | 
					        tasks.clear() | 
				
			||||||
 | 
					        searchPool?.close() | 
				
			||||||
 | 
					        mSearchId = 0L | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    interface CallBack { | 
				
			||||||
 | 
					        fun onSearchStart() | 
				
			||||||
 | 
					        fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) | 
				
			||||||
 | 
					        fun onSearchFinish() | 
				
			||||||
 | 
					        fun onSearchCancel() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,164 @@ | 
				
			|||||||
 | 
					package io.legado.app.ui.book.read.config | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.annotation.SuppressLint | 
				
			||||||
 | 
					import android.content.Context | 
				
			||||||
 | 
					import android.os.Bundle | 
				
			||||||
 | 
					import android.util.DisplayMetrics | 
				
			||||||
 | 
					import android.view.LayoutInflater | 
				
			||||||
 | 
					import android.view.MenuItem | 
				
			||||||
 | 
					import android.view.View | 
				
			||||||
 | 
					import android.view.ViewGroup | 
				
			||||||
 | 
					import androidx.appcompat.widget.Toolbar | 
				
			||||||
 | 
					import androidx.lifecycle.LiveData | 
				
			||||||
 | 
					import androidx.recyclerview.widget.LinearLayoutManager | 
				
			||||||
 | 
					import io.legado.app.App | 
				
			||||||
 | 
					import io.legado.app.R | 
				
			||||||
 | 
					import io.legado.app.base.BaseDialogFragment | 
				
			||||||
 | 
					import io.legado.app.base.adapter.ItemViewHolder | 
				
			||||||
 | 
					import io.legado.app.base.adapter.SimpleRecyclerAdapter | 
				
			||||||
 | 
					import io.legado.app.constant.PreferKey | 
				
			||||||
 | 
					import io.legado.app.data.entities.HttpTTS | 
				
			||||||
 | 
					import io.legado.app.lib.dialogs.alert | 
				
			||||||
 | 
					import io.legado.app.lib.dialogs.cancelButton | 
				
			||||||
 | 
					import io.legado.app.lib.dialogs.customView | 
				
			||||||
 | 
					import io.legado.app.lib.dialogs.okButton | 
				
			||||||
 | 
					import io.legado.app.lib.theme.primaryColor | 
				
			||||||
 | 
					import io.legado.app.service.help.ReadAloud | 
				
			||||||
 | 
					import io.legado.app.utils.* | 
				
			||||||
 | 
					import kotlinx.android.synthetic.main.dialog_http_tts_edit.view.* | 
				
			||||||
 | 
					import kotlinx.android.synthetic.main.dialog_recycler_view.* | 
				
			||||||
 | 
					import kotlinx.android.synthetic.main.item_http_tts.view.* | 
				
			||||||
 | 
					import org.jetbrains.anko.sdk27.listeners.onClick | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onStart() { | 
				
			||||||
 | 
					        super.onStart() | 
				
			||||||
 | 
					        val dm = DisplayMetrics() | 
				
			||||||
 | 
					        activity?.windowManager?.defaultDisplay?.getMetrics(dm) | 
				
			||||||
 | 
					        dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lateinit var adapter: Adapter | 
				
			||||||
 | 
					    lateinit var viewModel: SpeakEngineViewModel | 
				
			||||||
 | 
					    private var httpTTSData: LiveData<List<HttpTTS>>? = null | 
				
			||||||
 | 
					    var engineId = App.INSTANCE.getPrefLong(PreferKey.speakEngine) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onCreateView( | 
				
			||||||
 | 
					        inflater: LayoutInflater, | 
				
			||||||
 | 
					        container: ViewGroup?, | 
				
			||||||
 | 
					        savedInstanceState: Bundle? | 
				
			||||||
 | 
					    ): View? { | 
				
			||||||
 | 
					        viewModel = getViewModel(SpeakEngineViewModel::class.java) | 
				
			||||||
 | 
					        return inflater.inflate(R.layout.dialog_recycler_view, container) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { | 
				
			||||||
 | 
					        initView() | 
				
			||||||
 | 
					        initMenu() | 
				
			||||||
 | 
					        initData() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun initView() { | 
				
			||||||
 | 
					        tool_bar.setBackgroundColor(primaryColor) | 
				
			||||||
 | 
					        tool_bar.setTitle(R.string.speak_engine) | 
				
			||||||
 | 
					        recycler_view.layoutManager = LinearLayoutManager(requireContext()) | 
				
			||||||
 | 
					        adapter = Adapter(requireContext()) | 
				
			||||||
 | 
					        recycler_view.adapter = adapter | 
				
			||||||
 | 
					        tv_footer_left.setText(R.string.local_tts) | 
				
			||||||
 | 
					        tv_footer_left.visible() | 
				
			||||||
 | 
					        tv_footer_left.onClick { | 
				
			||||||
 | 
					            removePref(PreferKey.speakEngine) | 
				
			||||||
 | 
					            dismiss() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        tv_ok.visible() | 
				
			||||||
 | 
					        tv_ok.onClick { | 
				
			||||||
 | 
					            putPrefLong(PreferKey.speakEngine, engineId) | 
				
			||||||
 | 
					            dismiss() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        tv_cancel.visible() | 
				
			||||||
 | 
					        tv_cancel.onClick { | 
				
			||||||
 | 
					            dismiss() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun initMenu() { | 
				
			||||||
 | 
					        tool_bar.inflateMenu(R.menu.speak_engine) | 
				
			||||||
 | 
					        tool_bar.menu.applyTint(requireContext()) | 
				
			||||||
 | 
					        tool_bar.setOnMenuItemClickListener(this) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun initData() { | 
				
			||||||
 | 
					        httpTTSData?.removeObservers(this) | 
				
			||||||
 | 
					        httpTTSData = App.db.httpTTSDao().observeAll() | 
				
			||||||
 | 
					        httpTTSData?.observe(this, { | 
				
			||||||
 | 
					            adapter.setItems(it) | 
				
			||||||
 | 
					        }) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onMenuItemClick(item: MenuItem?): Boolean { | 
				
			||||||
 | 
					        when (item?.itemId) { | 
				
			||||||
 | 
					            R.id.menu_add -> editHttpTTS() | 
				
			||||||
 | 
					            R.id.menu_default -> viewModel.importDefault() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        return true | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @SuppressLint("InflateParams") | 
				
			||||||
 | 
					    private fun editHttpTTS(v: HttpTTS? = null) { | 
				
			||||||
 | 
					        val httpTTS = v?.copy() ?: HttpTTS() | 
				
			||||||
 | 
					        requireContext().alert(titleResource = R.string.speak_engine) { | 
				
			||||||
 | 
					            var rootView: View? = null | 
				
			||||||
 | 
					            customView { | 
				
			||||||
 | 
					                LayoutInflater.from(requireContext()) | 
				
			||||||
 | 
					                    .inflate(R.layout.dialog_http_tts_edit, null).apply { | 
				
			||||||
 | 
					                        rootView = this | 
				
			||||||
 | 
					                        tv_name.setText(httpTTS.name) | 
				
			||||||
 | 
					                        tv_url.setText(httpTTS.url) | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            cancelButton() | 
				
			||||||
 | 
					            okButton { | 
				
			||||||
 | 
					                rootView?.apply { | 
				
			||||||
 | 
					                    httpTTS.name = tv_name.text.toString() | 
				
			||||||
 | 
					                    httpTTS.url = tv_url.text.toString() | 
				
			||||||
 | 
					                    App.db.httpTTSDao().insert(httpTTS) | 
				
			||||||
 | 
					                    ReadAloud.upReadAloudClass() | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        }.show().applyTint() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inner class Adapter(context: Context) : | 
				
			||||||
 | 
					        SimpleRecyclerAdapter<HttpTTS>(context, R.layout.item_http_tts) { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        override fun convert(holder: ItemViewHolder, item: HttpTTS, payloads: MutableList<Any>) { | 
				
			||||||
 | 
					            holder.itemView.apply { | 
				
			||||||
 | 
					                cb_name.text = item.name | 
				
			||||||
 | 
					                cb_name.isChecked = item.id == engineId | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        override fun registerListener(holder: ItemViewHolder) { | 
				
			||||||
 | 
					            holder.itemView.apply { | 
				
			||||||
 | 
					                cb_name.onClick { | 
				
			||||||
 | 
					                    getItem(holder.layoutPosition)?.let { httpTTS -> | 
				
			||||||
 | 
					                        engineId = httpTTS.id | 
				
			||||||
 | 
					                        notifyItemRangeChanged(0, getActualItemCount()) | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                iv_edit.onClick { | 
				
			||||||
 | 
					                    editHttpTTS(getItem(holder.layoutPosition)) | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                iv_menu_delete.onClick { | 
				
			||||||
 | 
					                    getItem(holder.layoutPosition)?.let { httpTTS -> | 
				
			||||||
 | 
					                        App.db.httpTTSDao().delete(httpTTS) | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,34 @@ | 
				
			|||||||
 | 
					package io.legado.app.ui.book.read.config | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.Application | 
				
			||||||
 | 
					import io.legado.app.App | 
				
			||||||
 | 
					import io.legado.app.base.BaseViewModel | 
				
			||||||
 | 
					import io.legado.app.data.entities.TxtTocRule | 
				
			||||||
 | 
					import io.legado.app.help.DefaultValueHelp | 
				
			||||||
 | 
					import io.legado.app.help.http.HttpHelper | 
				
			||||||
 | 
					import io.legado.app.utils.GSON | 
				
			||||||
 | 
					import io.legado.app.utils.fromJsonArray | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SpeakEngineViewModel(application: Application) : BaseViewModel(application) { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun importDefault() { | 
				
			||||||
 | 
					        execute { | 
				
			||||||
 | 
					            DefaultValueHelp.initHttpTTS() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun importOnLine(url: String, finally: (msg: String) -> Unit) { | 
				
			||||||
 | 
					        execute { | 
				
			||||||
 | 
					            HttpHelper.simpleGetAsync(url)?.let { json -> | 
				
			||||||
 | 
					                GSON.fromJsonArray<TxtTocRule>(json)?.let { | 
				
			||||||
 | 
					                    App.db.txtTocRule().insert(*it.toTypedArray()) | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        }.onSuccess { | 
				
			||||||
 | 
					            finally("导入成功") | 
				
			||||||
 | 
					        }.onError { | 
				
			||||||
 | 
					            finally("导入失败") | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in new issue