diff --git a/app/src/main/assets/httpTTS.json b/app/src/main/assets/httpTTS.json new file mode 100644 index 000000000..29fda4c6f --- /dev/null +++ b/app/src/main/assets/httpTTS.json @@ -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}" + } +] \ No newline at end of file diff --git a/app/src/main/assets/readConfig.json b/app/src/main/assets/readConfig.json index 301a75705..968141a0c 100644 --- a/app/src/main/assets/readConfig.json +++ b/app/src/main/assets/readConfig.json @@ -1,9 +1,9 @@ [ { - "bgStr": "#EBD9BB", - "bgStrNight": "#1E2021", - "textColor": "#63543C", - "textColorNight": "#DCDFE1", + "bgStr": "#FFFFFF", + "bgStrNight": "#000000", + "textColor": "#000000", + "textColorNight": "#FFFFFF", "bgType": 0, "bgTypeNight": 0, "darkStatusIcon": true, diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 159dcf354..355fe1364 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -3,11 +3,56 @@ * 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 - 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 -**2020/08/23** +**2020/08/29** +* 修复一个null引起的崩溃bug +* 修复我的界面滚动时图标消失的bug +* 修复从详情页目录打开章节内容不对的bug +* 书源选择增加滑动选择, 选择框区域滑动时进行选择 by [Mupceet](https://github.com/Mupceet) +* 请求头,支持http代理,socks4 socks5代理设置 by [10bits](https://github.com/10bits) +``` +socks5代理 +{ + "proxy":"socks5://127.0.0.1:1080" +} +http代理 +{ + "proxy":"http://127.0.0.1:1080" +} +支持代理服务器验证 +{ + "proxy":"socks5://127.0.0.1:1080@用户名@密码" +} +注意:这些请求头是无意义的,会被忽略掉 +``` + +**2020/08/28** +* 修复一些bug +* 换源不再改变书名和作者,防止换到一些错误的书源后不能再换源 + +**2020/08/27** +* 修复主题bug +* 修复封面bug +* 优化书籍更新,搜索,换源 +* e-ink模式不再固定背景 + +**2020/08/26** +* js添加java.encodeURI(speakText),用于解决js编码时有~的语句朗读不出来 +* 修复书名太长删除阅读记录按钮不显示的bug +* 完成本地书籍编码选择 + +**2020/08/25** +* 阅读记录可以删除了 +* 修复翻页模式选择颜色问题 +* 修复toolbar在一些情况下文字颜色不对的bug +* 多设备阅读记录叠加 +* 封面链接支持修改headers + +**2020/08/24** * 应用被杀死时停止朗读 * 默认封面添加删除操作 * 备份阅读记录 * 书源添加移除分组支持多选,多个分组以逗号(中英均可)隔开 +* 可以自定义在线朗读了 **2020/08/22** * 添加阅读时间记录 diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index d14951cb1..a96db68fb 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Color import android.os.Build +import android.provider.Settings import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate import androidx.multidex.MultiDexApplication @@ -34,14 +35,16 @@ class App : MultiDexApplication() { @JvmStatic lateinit var db: AppDatabase private set - } - var versionCode = 0 - var versionName = "" + lateinit var androidId: String + var versionCode = 0 + var versionName = "" + } override fun onCreate() { super.onCreate() INSTANCE = this + androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID) CrashHandler().init(this) LanguageUtils.setConfigurationOld(this) db = AppDatabase.createDatabase(INSTANCE) diff --git a/app/src/main/java/io/legado/app/base/BaseActivity.kt b/app/src/main/java/io/legado/app/base/BaseActivity.kt index 0574813a2..3ca496783 100644 --- a/app/src/main/java/io/legado/app/base/BaseActivity.kt +++ b/app/src/main/java/io/legado/app/base/BaseActivity.kt @@ -25,6 +25,7 @@ abstract class BaseActivity( private val layoutID: Int, private val fullScreen: Boolean = true, private val theme: Theme = Theme.Auto, + private val toolBarTheme: Theme = Theme.Auto, private val transparent: Boolean = false ) : AppCompatActivity(), CoroutineScope by MainScope() { @@ -65,7 +66,7 @@ abstract class BaseActivity( final override fun onCreateOptionsMenu(menu: Menu?): Boolean { return menu?.let { val bool = onCompatCreateOptionsMenu(it) - it.applyTint(this, theme) + it.applyTint(this, toolBarTheme) bool } ?: super.onCreateOptionsMenu(menu) } @@ -129,10 +130,10 @@ abstract class BaseActivity( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE } ATH.setStatusBarColorAuto(this, fullScreen) - if (theme == Theme.Dark) { - ATH.setLightStatusBar(this, false) - } else if (theme == Theme.Light) { - ATH.setLightStatusBar(this, true) + if (toolBarTheme == Theme.Dark) { + ATH.setLightStatusBar(window, false) + } else if (toolBarTheme == Theme.Light) { + ATH.setLightStatusBar(window, true) } upNavigationBarColor() } diff --git a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt index 3643588af..f8de615de 100644 --- a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt +++ b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt @@ -6,8 +6,9 @@ import io.legado.app.constant.Theme abstract class VMBaseActivity( layoutID: Int, fullScreen: Boolean = true, - theme: Theme = Theme.Auto -) : BaseActivity(layoutID, fullScreen, theme) { + theme: Theme = Theme.Auto, + toolBarTheme: Theme = Theme.Auto +) : BaseActivity(layoutID, fullScreen, theme, toolBarTheme) { protected abstract val viewModel: VM diff --git a/app/src/main/java/io/legado/app/constant/AppPattern.kt b/app/src/main/java/io/legado/app/constant/AppPattern.kt index 396edb73d..2faf70daf 100644 --- a/app/src/main/java/io/legado/app/constant/AppPattern.kt +++ b/app/src/main/java/io/legado/app/constant/AppPattern.kt @@ -3,8 +3,14 @@ package io.legado.app.constant import java.util.regex.Pattern object AppPattern { - val JS_PATTERN: Pattern = Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) + val JS_PATTERN: Pattern = + Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") + val imgPattern: Pattern = + Pattern.compile("", Pattern.CASE_INSENSITIVE) - val authorRegex = "作\\s*者\\s*[::]".toRegex() + val nameRegex = Regex("\\s+作\\s*者.*") + val authorRegex = Regex(".*?作\\s*?者[::]") + val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") + val splitGroupRegex = Regex("[,;]") } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt index e24ba8000..6fe8b5cea 100644 --- a/app/src/main/java/io/legado/app/constant/EventBus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -3,7 +3,7 @@ package io.legado.app.constant object EventBus { const val MEDIA_BUTTON = "mediaButton" const val RECREATE = "RECREATE" - const val UP_BOOK = "sourceDebugLog" + const val UP_BOOK = "upBookToc" const val ALOUD_STATE = "aloud_state" const val TTS_PROGRESS = "ttsStart" const val TTS_DS = "ttsDs" diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index a4a9572bd..bf98afd11 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -9,10 +9,9 @@ object PreferKey { const val clickAllNext = "clickAllNext" const val hideNavigationBar = "hideNavigationBar" const val precisionSearch = "precisionSearch" - const val readAloudOnLine = "readAloudOnLine" + const val speakEngine = "speakEngine" const val readAloudByPage = "readAloudByPage" const val ttsSpeechRate = "ttsSpeechRate" - const val ttsSpeechPer = "ttsSpeechPer" const val prevKey = "prevKeyCode" const val nextKey = "nextKeyCode" const val showRss = "showRss" diff --git a/app/src/main/java/io/legado/app/constant/Theme.kt b/app/src/main/java/io/legado/app/constant/Theme.kt index a4f313a80..720c339f9 100644 --- a/app/src/main/java/io/legado/app/constant/Theme.kt +++ b/app/src/main/java/io/legado/app/constant/Theme.kt @@ -1,6 +1,7 @@ package io.legado.app.constant import io.legado.app.help.AppConfig +import io.legado.app.utils.ColorUtils enum class Theme { Dark, Light, Auto, Transparent; @@ -11,5 +12,13 @@ enum class Theme { Dark } else Light } + + fun getTheme(backgroundColor: Int): Theme { + return if (ColorUtils.isColorLight(backgroundColor)) { + Light + } else { + Dark + } + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index 71e6e453f..7d0a81128 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import io.legado.app.App import io.legado.app.data.dao.* import io.legado.app.data.entities.* @@ -14,8 +15,8 @@ import io.legado.app.data.entities.* entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, - RssStar::class, TxtTocRule::class, ReadRecord::class], - version = 17, + RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class], + version = 19, exportSchema = true ) abstract class AppDatabase : RoomDatabase() { @@ -33,7 +34,9 @@ abstract class AppDatabase : RoomDatabase() { migration_12_13, migration_13_14, migration_14_15, - migration_15_17 + migration_15_17, + migration_17_18, + migration_18_19 ) .allowMainThreadQueries() .build() @@ -94,6 +97,21 @@ abstract class AppDatabase : RoomDatabase() { database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))") } } + + private val migration_17_18 = object : Migration(17, 18) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))") + } + } + + private val migration_18_19 = object : Migration(18, 19) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))") + database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord") + database.execSQL("DROP TABLE readRecord") + database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord") + } + } } abstract fun bookDao(): BookDao @@ -110,4 +128,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun cookieDao(): CookieDao abstract fun txtTocRule(): TxtTocRuleDao abstract fun readRecordDao(): ReadRecordDao + abstract fun httpTTSDao(): HttpTTSDao } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt new file mode 100644 index 000000000..f71169bb1 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt @@ -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 + + @Query("select * from httpTTS order by name") + fun observeAll(): LiveData> + + @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) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt index 88c785d0a..311194cf0 100644 --- a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt @@ -2,6 +2,7 @@ package io.legado.app.data.dao import androidx.room.* import io.legado.app.data.entities.ReadRecord +import io.legado.app.data.entities.ReadRecordShow @Dao interface ReadRecordDao { @@ -9,16 +10,27 @@ interface ReadRecordDao { @get:Query("select * from readRecord") val all: List + @get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName") + val allShow: List + @get:Query("select sum(readTime) from readRecord") val allTime: Long - @Query("select readTime from readRecord where bookName = :bookName") + @Query("select sum(readTime) from readRecord where bookName = :bookName") fun getReadTime(bookName: String): Long? + @Query("select readTime from readRecord where androidId = :androidId and bookName = :bookName") + fun getReadTime(androidId: String, bookName: String): Long? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg readRecord: ReadRecord) @Update fun update(vararg record: ReadRecord) + @Delete + fun delete(vararg record: ReadRecord) + + @Query("delete from readRecord where bookName = :bookName") + fun deleteByName(bookName: String) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index 33d5d309b..c6759c237 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -11,6 +11,7 @@ import io.legado.app.constant.BookType import io.legado.app.help.AppConfig import io.legado.app.service.help.ReadBook import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils import io.legado.app.utils.fromJsonObject import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize @@ -115,6 +116,10 @@ data class Book( return charset(charset ?: "UTF-8") } + fun getFolderName(): String { + return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl) + } + fun toSearchBook(): SearchBook { return SearchBook( name = name, diff --git a/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt b/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt new file mode 100644 index 000000000..2e5ad7df0 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt @@ -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 = "" +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt index 488dc0205..60f577477 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt @@ -1,11 +1,10 @@ package io.legado.app.data.entities import androidx.room.Entity -import androidx.room.PrimaryKey -@Entity(tableName = "readRecord") +@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"]) data class ReadRecord( - @PrimaryKey + var androidId: String = "", var bookName: String = "", var readTime: Long = 0L ) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt b/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt new file mode 100644 index 000000000..79795b0d9 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt @@ -0,0 +1,6 @@ +package io.legado.app.data.entities + +data class ReadRecordShow( + var bookName: String = "", + var readTime: Long = 0L +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index 8157af173..aebc822b7 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -81,9 +81,6 @@ object AppConfig { App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value) } - val ttsSpeechPer: String - get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0" - val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) var chineseConverterType: Int diff --git a/app/src/main/java/io/legado/app/help/BookHelp.kt b/app/src/main/java/io/legado/app/help/BookHelp.kt index 7925fccf8..94e7fdde1 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -2,6 +2,7 @@ package io.legado.app.help import com.hankcs.hanlp.HanLP import io.legado.app.App +import io.legado.app.constant.AppPattern import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter @@ -16,19 +17,12 @@ import kotlinx.coroutines.withContext import org.apache.commons.text.similarity.JaccardSimilarity import org.jetbrains.anko.toast import java.io.File -import java.util.regex.Pattern import kotlin.math.min object BookHelp { private const val cacheFolderName = "book_cache" private const val cacheImageFolderName = "images" private val downloadDir: File = App.INSTANCE.externalFilesDir - private val srcPattern = - Pattern.compile("", Pattern.CASE_INSENSITIVE) - - fun bookFolderName(book: Book): String { - return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl) - } fun formatChapterName(bookChapter: BookChapter): String { return String.format( @@ -51,7 +45,7 @@ object BookHelp { Coroutine.async { val bookFolderNames = arrayListOf() App.db.bookDao().all.forEach { - bookFolderNames.add(bookFolderName(it)) + bookFolderNames.add(it.getFolderName()) } val file = FileUtils.getDirFile(downloadDir, cacheFolderName) file.listFiles()?.forEach { bookFile -> @@ -62,18 +56,17 @@ object BookHelp { } } - @Synchronized fun saveContent(book: Book, bookChapter: BookChapter, content: String) { if (content.isEmpty()) return //保存文本 FileUtils.createFileIfNotExist( downloadDir, formatChapterName(bookChapter), - subDirs = arrayOf(cacheFolderName, bookFolderName(book)) + subDirs = arrayOf(cacheFolderName, book.getFolderName()) ).writeText(content) //保存图片 content.split("\n").forEach { - val matcher = srcPattern.matcher(it) + val matcher = AppPattern.imgPattern.matcher(it) if (matcher.find()) { var src = matcher.group(1) src = NetworkUtils.getAbsoluteURL(bookChapter.url, src) @@ -86,29 +79,40 @@ object BookHelp { } fun saveImage(book: Book, src:String) { - val analyzeUrl = AnalyzeUrl(src, null, null, null, null) + val analyzeUrl = AnalyzeUrl(src) analyzeUrl.getImageBytes(book.origin)?.let { FileUtils.createFileIfNotExist( downloadDir, - "${MD5Utils.md5Encode16(src)}${src.substringAfterLast(".").substringBefore(",")}", - subDirs = arrayOf(cacheFolderName, bookFolderName(book), cacheImageFolderName) + "${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}", + subDirs = arrayOf(cacheFolderName, book.getFolderName(), cacheImageFolderName) ).writeBytes(it) } } - - fun getImage(book:Book, src:String): File { + + fun getImage(book: Book, src: String): File { return FileUtils.getFile( downloadDir, - "${MD5Utils.md5Encode16(src)}${src.substringAfterLast(".").substringBefore(",")}", - subDirs = arrayOf(cacheFolderName, bookFolderName(book), cacheImageFolderName) + "${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}", + subDirs = arrayOf(cacheFolderName, book.getFolderName(), cacheImageFolderName) ) } + private fun getImageSuffix(src: String): String { + var suffix = src.substringAfterLast(".").substringBefore(",") + if (suffix.length > 5) { + suffix = ".jpg" + } + return suffix + } + fun getChapterFiles(book: Book): List { val fileNameList = arrayListOf() + if (book.isLocalBook()) { + return fileNameList + } FileUtils.createFolderIfNotExist( downloadDir, - subDirs = arrayOf(cacheFolderName, bookFolderName(book)) + subDirs = arrayOf(cacheFolderName, book.getFolderName()) ).list()?.let { fileNameList.addAll(it) } @@ -122,7 +126,7 @@ object BookHelp { FileUtils.exists( downloadDir, formatChapterName(bookChapter), - subDirs = arrayOf(cacheFolderName, bookFolderName(book)) + subDirs = arrayOf(cacheFolderName, book.getFolderName()) ) } } @@ -134,7 +138,7 @@ object BookHelp { val file = FileUtils.getFile( downloadDir, formatChapterName(bookChapter), - subDirs = arrayOf(cacheFolderName, bookFolderName(book)) + subDirs = arrayOf(cacheFolderName, book.getFolderName()) ) if (file.exists()) { return file.readText() @@ -150,24 +154,20 @@ object BookHelp { FileUtils.createFileIfNotExist( downloadDir, formatChapterName(bookChapter), - subDirs = arrayOf(cacheFolderName, bookFolderName(book)) + subDirs = arrayOf(cacheFolderName, book.getFolderName()) ).delete() } } - private fun formatFolderName(folderName: String): String { - return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "") - } - fun formatBookName(name: String): String { return name - .replace("\\s+作\\s*者.*".toRegex(), "") + .replace(AppPattern.nameRegex, "") .trim { it <= ' ' } } fun formatBookAuthor(author: String): String { return author - .replace(".*?作\\s*?者[::]".toRegex(), "") + .replace(AppPattern.authorRegex, "") .trim { it <= ' ' } } diff --git a/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt b/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt new file mode 100644 index 000000000..49338e960 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt @@ -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(json)?.let { + App.db.httpTTSDao().insert(*it.toTypedArray()) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ImageLoader.kt b/app/src/main/java/io/legado/app/help/ImageLoader.kt index 11f8dff21..9a58f45bf 100644 --- a/app/src/main/java/io/legado/app/help/ImageLoader.kt +++ b/app/src/main/java/io/legado/app/help/ImageLoader.kt @@ -9,6 +9,7 @@ import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isContentPath +import io.legado.app.model.analyzeRule.AnalyzeUrl import java.io.File object ImageLoader { @@ -19,7 +20,7 @@ object ImageLoader { fun load(context: Context, path: String?): RequestBuilder { return when { path.isNullOrEmpty() -> Glide.with(context).load(path) - path.isAbsUrl() -> Glide.with(context).load(path) + path.isAbsUrl() -> Glide.with(context).load(AnalyzeUrl(path).getGlideUrl()) path.isContentPath() -> Glide.with(context).load(Uri.parse(path)) else -> try { Glide.with(context).load(File(path)) diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index d12fc07ea..cea6359e3 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -6,6 +6,7 @@ import io.legado.app.constant.AppConst.dateFormat import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.EncoderUtils import io.legado.app.utils.MD5Utils +import java.net.URLEncoder import java.util.* @Keep @@ -52,10 +53,19 @@ interface JsExtensions { fun timeFormat(time: Long): String { return dateFormat.format(Date(time)) } + //utf8编码转gbk编码 fun utf8ToGbk(str: String): String { val utf8 = String(str.toByteArray(charset("UTF-8"))) val unicode = String(utf8.toByteArray(), charset("UTF-8")) return String(unicode.toByteArray(charset("GBK"))) } + + fun encodeURI(str: String): String { + return try { + URLEncoder.encode(str, "UTF-8") + } catch (e: Exception) { + "" + } + } } diff --git a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt index 73e344393..c6c71598c 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -29,13 +29,7 @@ object ReadBookConfig { var bg: Drawable? = null var bgMeanColor: Int = 0 - val textColor: Int - get() = - if (AppConfig.isEInkMode && !AppConfig.isNightTheme) { - Color.BLACK - } else { - durConfig.textColor() - } + val textColor: Int get() = durConfig.textColor() init { upConfig() @@ -77,16 +71,11 @@ object ReadBookConfig { val dm = resources.displayMetrics val width = dm.widthPixels val height = dm.heightPixels - if (AppConfig.isEInkMode && !AppConfig.isNightTheme) { - bg = ColorDrawable(Color.WHITE) - bgMeanColor = Color.WHITE - } else { - bg = durConfig.bgDrawable(width, height).apply { - if (this is BitmapDrawable) { - bgMeanColor = BitmapUtils.getMeanColor(bitmap) - } else if (this is ColorDrawable) { - bgMeanColor = color - } + bg = durConfig.bgDrawable(width, height).apply { + if (this is BitmapDrawable) { + bgMeanColor = BitmapUtils.getMeanColor(bitmap) + } else if (this is ColorDrawable) { + bgMeanColor = color } } isScroll = pageAnim == 3 diff --git a/app/src/main/java/io/legado/app/help/http/CookieStore.kt b/app/src/main/java/io/legado/app/help/http/CookieStore.kt index 3f40cfbe0..067435ddb 100644 --- a/app/src/main/java/io/legado/app/help/http/CookieStore.kt +++ b/app/src/main/java/io/legado/app/help/http/CookieStore.kt @@ -6,13 +6,13 @@ import com.franmontiel.persistentcookiejar.persistence.SerializableCookie import io.legado.app.App import io.legado.app.data.entities.Cookie import io.legado.app.help.coroutine.Coroutine - +import io.legado.app.utils.NetworkUtils object CookieStore : CookiePersistor { fun setCookie(url: String, cookie: String?) { Coroutine.async { - val cookieBean = Cookie(url, cookie ?: "") + val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "") App.db.cookieDao().insert(cookieBean) } } @@ -33,12 +33,12 @@ object CookieStore : CookiePersistor { } fun getCookie(url: String): String { - val cookieBean = App.db.cookieDao().get(url) + val cookieBean = App.db.cookieDao().get(NetworkUtils.getSubDomain(url)) return cookieBean?.cookie ?: "" } fun removeCookie(url: String) { - App.db.cookieDao().delete(url) + App.db.cookieDao().delete(NetworkUtils.getSubDomain(url)) } private fun cookieToMap(cookie: String): MutableMap { diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index 51103b52e..6364e9ed8 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -4,11 +4,10 @@ import io.legado.app.constant.AppConst import io.legado.app.help.http.api.HttpGetApi import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.ConnectionSpec -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Protocol +import okhttp3.* import retrofit2.Retrofit +import java.net.InetSocketAddress +import java.net.Proxy import java.util.concurrent.TimeUnit import kotlin.coroutines.resume @@ -83,6 +82,18 @@ object HttpHelper { return getRetrofit(baseUrl, encode).create(T::class.java) } + inline fun getApiServiceWithProxy( + baseUrl: String, + encode: String? = null, + proxy: String? = null + ): T { + return getRetrofitWithProxy(baseUrl, encode, proxy).create(T::class.java) + } + + inline fun getBytesApiService(baseUrl: String): T { + return getByteRetrofit(baseUrl).create(T::class.java) + } + fun getRetrofit(baseUrl: String, encode: String? = null): Retrofit { return Retrofit.Builder().baseUrl(baseUrl) //增加返回值为字符串的支持(以实体类返回) @@ -91,6 +102,56 @@ object HttpHelper { .build() } + fun getRetrofitWithProxy( + baseUrl: String, + encode: String? = null, + proxy: String? = null + ): Retrofit { + val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?") + val ms = proxy?.let { r.findAll(it) }; + val group = ms?.first() + var type = "direct" //直接连接 + var host = "127.0.0.1" //代理服务器hostname + var port = 1080 //代理服务器port + var username = "" //代理服务器验证用户名 + var password = "" //代理服务器验证密码 + if (group != null) { + type = if (group.groupValues[1] == "http") { + "http" + } else { + "socks" + } + host = group.groupValues[2] + port = group.groupValues[3].toInt() + if (group.groupValues[4] != "") { + username = group.groupValues[4].split("@")[1] + password = group.groupValues[4].split("@")[2] + } + } + val builder = client.newBuilder() + if (type != "direct" && host != "") { + if (type == "http") { + builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))); + } else { + builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port))); + } + if (username != "" && password != "") { + builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 + val credential: String = Credentials.basic(username, password) + response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + } + + } + return Retrofit.Builder().baseUrl(baseUrl) + //增加返回值为字符串的支持(以实体类返回) + .addConverterFactory(EncodeConverter(encode)) + .client(builder.build()) + .build() + } + fun getByteRetrofit(baseUrl: String): Retrofit { return Retrofit.Builder().baseUrl(baseUrl) .addConverterFactory(ByteConverter()) diff --git a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt index 924361489..7138c8c20 100644 --- a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt +++ b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt @@ -32,6 +32,12 @@ interface HttpGetApi { @HeaderMap headers: Map ): Call + @GET + fun getByte( + @Url url: String, + @HeaderMap headers: Map + ): Call + @GET fun getMap( @Url url: String, @@ -46,6 +52,12 @@ interface HttpGetApi { @HeaderMap headers: Map ): Call + @GET + suspend fun getByteAsync( + @Url url: String, + @HeaderMap headers: Map + ): Response + @GET suspend fun getMapByteAsync( @Url url: String, diff --git a/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt index 9634b5b79..170894390 100644 --- a/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt +++ b/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt @@ -49,4 +49,12 @@ interface HttpPostApi { @FieldMap(encoded = true) fieldMap: Map, @HeaderMap headers: Map ): Response + + @POST + suspend fun postBodyByteAsync( + @Url url: String, + @Body body: RequestBody, + @HeaderMap headers: Map + ): Response + } diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index c431de070..bec27b53a 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -32,6 +32,7 @@ object Backup { "replaceRule.json", "txtTocRule.json", "readRecord.json", + "httpTTS.json", ReadBookConfig.readConfigFileName, "config.xml" ) @@ -59,6 +60,7 @@ object Backup { writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath) writeListToJson(App.db.txtTocRule().all, "txtTocRule.json", backupPath) writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath) + writeListToJson(App.db.httpTTSDao().all, "httpTTS.json", backupPath) GSON.toJson(ReadBookConfig.configList)?.let { FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName) .writeText(it) diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index ca73c6df4..5deeac5dd 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -39,7 +39,8 @@ object Restore { "readConfig", PreferKey.themeMode, PreferKey.bookshelfLayout, - PreferKey.showRss + PreferKey.showRss, + PreferKey.threadCount ) //忽略标题 @@ -47,7 +48,8 @@ object Restore { App.INSTANCE.getString(R.string.read_config), App.INSTANCE.getString(R.string.theme_mode), App.INSTANCE.getString(R.string.bookshelf_layout), - App.INSTANCE.getString(R.string.show_rss) + App.INSTANCE.getString(R.string.show_rss), + App.INSTANCE.getString(R.string.thread_count) ) //默认忽略keys @@ -135,7 +137,21 @@ object Restore { App.db.txtTocRule().insert(*it.toTypedArray()) } fileToListT(path, "readRecord.json")?.let { - App.db.readRecordDao().insert(*it.toTypedArray()) + it.forEach { readRecord -> + //判断是不是本机记录 + if (readRecord.androidId != App.androidId) { + App.db.readRecordDao().insert(readRecord) + } else { + val time = App.db.readRecordDao() + .getReadTime(readRecord.androidId, readRecord.bookName) + if (time == null || time < readRecord.readTime) { + App.db.readRecordDao().insert(readRecord) + } + } + } + } + fileToListT(path, "httpTTS.json")?.let { + App.db.httpTTSDao().insert(*it.toTypedArray()) } } } @@ -205,6 +221,7 @@ object Restore { PreferKey.themeMode == key && ignoreThemeMode -> false PreferKey.bookshelfLayout == key && ignoreBookshelfLayout -> false PreferKey.showRss == key && ignoreShowRss -> false + PreferKey.threadCount == key && ignoreThreadCount -> false else -> true } } @@ -217,6 +234,8 @@ object Restore { get() = ignoreConfig[PreferKey.bookshelfLayout] == true private val ignoreShowRss: Boolean get() = ignoreConfig[PreferKey.showRss] == true + private val ignoreThreadCount: Boolean + get() = ignoreConfig[PreferKey.threadCount] == true fun saveIgnoreConfig() { val json = GSON.toJson(ignoreConfig) diff --git a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt index 190ec2176..e728c28de 100644 --- a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt +++ b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt @@ -51,28 +51,25 @@ object WebDavHelp { return false } + @Throws(Exception::class) private fun getWebDavFileNames(): ArrayList { val url = rootWebDavUrl val names = arrayListOf() if (initWebDav()) { - try { - var files = WebDav(url).listFiles() - files = files.reversed() - for (index: Int in 0 until min(10, files.size)) { - files[index].displayName?.let { - names.add(it) - } + var files = WebDav(url).listFiles() + files = files.reversed() + for (index: Int in 0 until min(10, files.size)) { + files[index].displayName?.let { + names.add(it) } - } catch (e: Exception) { - return names } } return names } - suspend fun showRestoreDialog(context: Context): Boolean { + suspend fun showRestoreDialog(context: Context) { val names = withContext(IO) { getWebDavFileNames() } - return if (names.isNotEmpty()) { + if (names.isNotEmpty()) { withContext(Main) { context.selector( title = context.getString(R.string.select_restore_file), @@ -83,9 +80,8 @@ object WebDavHelp { } } } - true } else { - false + throw Exception("Web dav no back up file") } } diff --git a/app/src/main/java/io/legado/app/lib/theme/ATH.kt b/app/src/main/java/io/legado/app/lib/theme/ATH.kt index d9ee9e2a2..f69c5d40a 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ATH.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ATH.kt @@ -9,6 +9,7 @@ import android.graphics.drawable.GradientDrawable import android.os.Build import android.view.View import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR +import android.view.Window import android.widget.EdgeEffect import android.widget.ScrollView import androidx.annotation.ColorInt @@ -63,16 +64,16 @@ object ATH { } else { activity.window.statusBarColor = color } - setLightStatusBarAuto(activity, color) + setLightStatusBarAuto(activity.window, color) } - fun setLightStatusBarAuto(activity: Activity, bgColor: Int) { - setLightStatusBar(activity, ColorUtils.isColorLight(bgColor)) + fun setLightStatusBarAuto(window: Window, bgColor: Int) { + setLightStatusBar(window, ColorUtils.isColorLight(bgColor)) } - fun setLightStatusBar(activity: Activity, enabled: Boolean) { + fun setLightStatusBar(window: Window, enabled: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val decorView = activity.window.decorView + val decorView = window.decorView val systemUiVisibility = decorView.systemUiVisibility if (enabled) { decorView.systemUiVisibility = diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt index 0a93266b6..1b6731098 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt +++ b/app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt @@ -34,7 +34,6 @@ class ATERadioNoButton(context: Context, attrs: AttributeSet) : isBottomBackground -> { val isLight = ColorUtils.isColorLight(context.bottomBackground) val textColor = context.getPrimaryTextColor(isLight) - setTextColor(textColor) background = Selector.shapeBuild() .setCornerRadius(2.dp) .setStrokeWidth(2.dp) @@ -42,15 +41,28 @@ class ATERadioNoButton(context: Context, attrs: AttributeSet) : .setCheckedStrokeColor(context.accentColor) .setDefaultStrokeColor(textColor) .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(textColor) + .setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor))) + .create() + ) } else -> { + val textColor = context.getCompatColor(R.color.primaryText) background = Selector.shapeBuild() .setCornerRadius(2.dp) .setStrokeWidth(2.dp) .setCheckedBgColor(context.accentColor) .setCheckedStrokeColor(context.accentColor) - .setDefaultStrokeColor(context.getCompatColor(R.color.primaryText)) + .setDefaultStrokeColor(textColor) .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(textColor) + .setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor))) + .create() + ) } } diff --git a/app/src/main/java/io/legado/app/model/Debug.kt b/app/src/main/java/io/legado/app/model/Debug.kt index 85c87812c..795170985 100644 --- a/app/src/main/java/io/legado/app/model/Debug.kt +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -6,6 +6,8 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.model.rss.Rss +import io.legado.app.model.webBook.WebBook import io.legado.app.utils.htmlFormat import io.legado.app.utils.isAbsUrl import java.text.SimpleDateFormat diff --git a/app/src/main/java/io/legado/app/model/SearchBookModel.kt b/app/src/main/java/io/legado/app/model/SearchBookModel.kt deleted file mode 100644 index 1f6cac065..000000000 --- a/app/src/main/java/io/legado/app/model/SearchBookModel.kt +++ /dev/null @@ -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) - fun onSearchFinish() - fun onSearchCancel() - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index 9b1e7d3b0..271eb5f68 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -609,7 +609,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { */ override fun ajax(urlStr: String): String? { return try { - val analyzeUrl = AnalyzeUrl(urlStr, null, null, null, baseUrl, book) + val analyzeUrl = AnalyzeUrl(urlStr, baseUrl = baseUrl, book = book) val call = analyzeUrl.getResponse(urlStr) val response = call.execute() response.body() diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 9cbf64f8f..3fa15df00 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -3,6 +3,8 @@ package io.legado.app.model.analyzeRule import android.annotation.SuppressLint import android.text.TextUtils import androidx.annotation.Keep +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppConst.UA_NAME import io.legado.app.constant.AppConst.userAgent @@ -23,7 +25,6 @@ import java.util.* import java.util.regex.Pattern import javax.script.SimpleBindings - /** * Created by GKF on 2018/1/24. * 搜索URL规则解析 @@ -34,10 +35,12 @@ class AnalyzeUrl( var ruleUrl: String, key: String? = null, page: Int? = null, + speakText: String? = null, + speakSpeed: Int? = null, headerMapF: Map? = null, baseUrl: String? = null, book: BaseBook? = null, - var useWebView: Boolean = false + var useWebView: Boolean = false, ) : JsExtensions { companion object { private val pagePattern = Pattern.compile("<(.*?)>") @@ -47,8 +50,7 @@ class AnalyzeUrl( private var baseUrl: String = "" lateinit var url: String private set - var path: String? = null - private set + private lateinit var urlHasQuery: String val headerMap = HashMap() private var queryStr: String? = null private val fieldMap = LinkedHashMap() @@ -56,20 +58,34 @@ class AnalyzeUrl( private var body: String? = null private var requestBody: RequestBody? = null private var method = RequestMethod.GET + private val splitUrlRegex = Regex(",\\s*(?=\\{)") + private var proxy: String? = null init { baseUrl?.let { - this.baseUrl = it.split(",[^\\{]*".toRegex(), 1)[0] + this.baseUrl = it.split(splitUrlRegex, 1)[0] + } + headerMapF?.let { + headerMap.putAll(it) + if (it.containsKey("proxy")) { + proxy = it["proxy"] + headerMap.remove("proxy") + } } - headerMapF?.let { headerMap.putAll(it) } //替换参数 - analyzeJs(key, page, book) - replaceKeyPageJs(key, page, book) + analyzeJs(key, page, speakText, speakSpeed, book) + replaceKeyPageJs(key, page, speakText, speakSpeed, book) //处理URL initUrl() } - private fun analyzeJs(key: String?, page: Int?, book: BaseBook?) { + private fun analyzeJs( + key: String?, + page: Int?, + speakText: String?, + speakSpeed: Int?, + book: BaseBook?, + ) { val ruleList = arrayListOf() var start = 0 var tmp: String @@ -96,11 +112,13 @@ class AnalyzeUrl( when { ruleStr.startsWith("") -> { ruleStr = ruleStr.substring(4, ruleStr.lastIndexOf("<")) - ruleUrl = evalJS(ruleStr, ruleUrl, page, key, book) as String + ruleUrl = + evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String } ruleStr.startsWith("@js", true) -> { ruleStr = ruleStr.substring(4) - ruleUrl = evalJS(ruleStr, ruleUrl, page, key, book) as String + ruleUrl = + evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String } else -> ruleUrl = ruleStr.replace("@result", ruleUrl) } @@ -110,7 +128,13 @@ class AnalyzeUrl( /** * 替换关键字,页数,JS */ - private fun replaceKeyPageJs(key: String?, page: Int?, book: BaseBook?) { + private fun replaceKeyPageJs( + key: String?, + page: Int?, + speakText: String?, + speakSpeed: Int?, + book: BaseBook?, + ) { //page page?.let { val matcher = pagePattern.matcher(ruleUrl) @@ -132,10 +156,14 @@ class AnalyzeUrl( simpleBindings["baseUrl"] = baseUrl simpleBindings["page"] = page simpleBindings["key"] = key + simpleBindings["speakText"] = speakText + simpleBindings["speakSpeed"] = speakSpeed simpleBindings["book"] = book val expMatcher = EXP_PATTERN.matcher(ruleUrl) while (expMatcher.find()) { - jsEval = SCRIPT_ENGINE.eval(expMatcher.group(1), simpleBindings) + jsEval = expMatcher.group(1)?.let { + SCRIPT_ENGINE.eval(it, simpleBindings) + } ?: "" if (jsEval is String) { expMatcher.appendReplacement(sb, jsEval) } else if (jsEval is Double && jsEval % 1.0 == 0.0) { @@ -153,8 +181,9 @@ class AnalyzeUrl( * 处理URL */ private fun initUrl() { - var urlArray = ruleUrl.split(",[^\\{]*".toRegex(), 2) + var urlArray = ruleUrl.split(splitUrlRegex, 2) url = urlArray[0] + urlHasQuery = urlArray[0] NetworkUtils.getBaseUrl(url)?.let { baseUrl = it } @@ -172,6 +201,7 @@ class AnalyzeUrl( ?.let { headerMap.putAll(it) } } } + headerMap[UA_NAME] = headerMap[UA_NAME] ?: userAgent charset = option.charset body = if (option.body is String) { option.body @@ -242,12 +272,16 @@ class AnalyzeUrl( result: Any?, page: Int?, key: String?, - book: BaseBook? + speakText: String?, + speakSpeed: Int?, + book: BaseBook?, ): Any { val bindings = SimpleBindings() bindings["java"] = this bindings["page"] = page bindings["key"] = key + bindings["speakText"] = speakText + bindings["speakSpeed"] = speakSpeed bindings["book"] = book bindings["result"] = result bindings["baseUrl"] = baseUrl @@ -285,7 +319,7 @@ class AnalyzeUrl( suspend fun getResponseAwait( tag: String, jsStr: String? = null, - sourceRegex: String? = null + sourceRegex: String? = null, ): Res { if (useWebView) { val params = AjaxWebView.AjaxParams(url) @@ -304,51 +338,110 @@ class AnalyzeUrl( val res = when { method == RequestMethod.POST -> { if (fieldMap.isNotEmpty()) { + if (proxy == null) { + HttpHelper + .getApiService(baseUrl, charset) + .postMapAsync(url, fieldMap, headerMap) + } else { + HttpHelper + .getApiServiceWithProxy(baseUrl, charset, proxy) + .postMapAsync(url, fieldMap, headerMap) + } + } else { + if (proxy == null) { + HttpHelper + .getApiService(baseUrl, charset) + .postBodyAsync(url, requestBody!!, headerMap) + } else { + HttpHelper + .getApiServiceWithProxy(baseUrl, charset, proxy) + .postBodyAsync(url, requestBody!!, headerMap) + } + } + } + fieldMap.isEmpty() -> { + if (proxy == null) { HttpHelper - .getApiService(baseUrl, charset) - .postMapAsync(url, fieldMap, headerMap) + .getApiService(baseUrl, charset) + .getAsync(url, headerMap) + } else { HttpHelper - .getApiService(baseUrl, charset) - .postBodyAsync(url, requestBody!!, headerMap) + .getApiServiceWithProxy(baseUrl, charset, proxy) + .getAsync(url, headerMap) } + + } + else -> { + if (proxy == null) { + HttpHelper + .getApiService(baseUrl, charset) + .getMapAsync(url, fieldMap, headerMap) + } else { + HttpHelper + .getApiServiceWithProxy(baseUrl, charset, proxy) + .getMapAsync(url, fieldMap, headerMap) + } + } - fieldMap.isEmpty() -> HttpHelper - .getApiService(baseUrl, charset) - .getAsync(url, headerMap) - else -> HttpHelper - .getApiService(baseUrl, charset) - .getMapAsync(url, fieldMap, headerMap) } return Res(NetworkUtils.getUrl(res), res.body()) } @Throws(Exception::class) - fun getImageBytes( - tag: String - ): ByteArray? { - //资源为本站的资源,保留cookie - //图片盗链的不保留当前的cookie,可由js生成图片源站的cookie + fun getImageBytes(tag: String): ByteArray? { val cookie = CookieStore.getCookie(tag) - NetworkUtils.getBaseUrl(url)?.let { - val regex: Any - if(it.lastIndexOf(".") != it.indexOf(".")) { - regex = it.substring(it.indexOf(".")+1).toRegex() - } else { - regex = it.substring(it.lastIndexOf("/")+1).toRegex() + if (cookie.isNotEmpty()) { + headerMap["Cookie"] += cookie + } + return if (fieldMap.isEmpty()) { + HttpHelper.getBytes(url, mapOf(), headerMap) + } else { + HttpHelper.getBytes(url, fieldMap, headerMap) + } + } + + @Throws(Exception::class) + suspend fun getResponseBytes(tag: String? = null): ByteArray? { + if (tag != null) { + val cookie = CookieStore.getCookie(tag) + if (cookie.isNotEmpty()) { + headerMap["Cookie"] = cookie } - if(regex.containsMatchIn(tag)) { - if (cookie.isNotEmpty()) { - headerMap["Cookie"] += cookie + } + val response = when { + method == RequestMethod.POST -> { + if (fieldMap.isNotEmpty()) { + HttpHelper + .getBytesApiService(baseUrl) + .postMapByteAsync(url, fieldMap, headerMap) + } else { + HttpHelper + .getBytesApiService(baseUrl) + .postBodyByteAsync(url, requestBody!!, headerMap) } } + fieldMap.isEmpty() -> HttpHelper + .getBytesApiService(baseUrl) + .getByteAsync(url, headerMap) + else -> HttpHelper + .getBytesApiService(baseUrl) + .getMapByteAsync(url, fieldMap, headerMap) } - headerMap[UA_NAME] = headerMap[UA_NAME] ?:userAgent - if(fieldMap.isEmpty()) { - return HttpHelper.getBytes(url, mapOf(), headerMap) - } else { - return HttpHelper.getBytes(url, fieldMap, headerMap) + return response.body() + } + + @Throws(Exception::class) + fun getGlideUrl(): Any? { + var glideUrl: Any = urlHasQuery + if (headerMap.isNotEmpty()) { + val headers = LazyHeaders.Builder() + headerMap.forEach { (key, value) -> + headers.addHeader(key, value) + } + glideUrl = GlideUrl(urlHasQuery, headers.build()) } + return glideUrl } data class UrlOption( @@ -356,7 +449,7 @@ class AnalyzeUrl( val charset: String?, val webView: Any?, val headers: Any?, - val body: Any? + val body: Any?, ) } diff --git a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt b/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt index c4bfb6ea6..41caca4a9 100644 --- a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt @@ -20,7 +20,9 @@ class AnalyzeTxtFile { @Throws(Exception::class) fun analyze(book: Book): ArrayList { val bookFile = getBookFile(book) - book.charset = EncodingDetect.getEncode(bookFile) + if (book.charset == null) { + book.charset = EncodingDetect.getEncode(bookFile) + } charset = book.fileCharset() val rulePattern = if (book.tocUrl.isNotEmpty()) { Pattern.compile(book.tocUrl, Pattern.MULTILINE) diff --git a/app/src/main/java/io/legado/app/model/Rss.kt b/app/src/main/java/io/legado/app/model/rss/Rss.kt similarity index 98% rename from app/src/main/java/io/legado/app/model/Rss.kt rename to app/src/main/java/io/legado/app/model/rss/Rss.kt index 7b6d2e266..f9ea78195 100644 --- a/app/src/main/java/io/legado/app/model/Rss.kt +++ b/app/src/main/java/io/legado/app/model/rss/Rss.kt @@ -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.RssSource diff --git a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt index ce588c5cd..7d6c3a94c 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -2,7 +2,6 @@ package io.legado.app.model.webBook import io.legado.app.App import io.legado.app.R -import io.legado.app.constant.AppPattern import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.help.BookHelp @@ -19,7 +18,8 @@ object BookInfo { book: Book, body: String?, bookSource: BookSource, - baseUrl: String + baseUrl: String, + canReName: Boolean, ) { body ?: throw Exception( App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) @@ -35,13 +35,17 @@ object BookInfo { } } Debug.log(bookSource.bookSourceUrl, "┌获取书名") - BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).trim { it <= ' ' }.let { - if (it.isNotEmpty()) book.name = it + BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let { + if (it.isNotEmpty() && (canReName || book.name.isEmpty())) { + book.name = it + } } Debug.log(bookSource.bookSourceUrl, "└${book.name}") Debug.log(bookSource.bookSourceUrl, "┌获取作者") BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let { - if (it.isNotEmpty()) book.author = it.replace(AppPattern.authorRegex, "") + if (it.isNotEmpty() && (canReName || book.name.isEmpty())) { + book.author = it + } } Debug.log(bookSource.bookSourceUrl, "└${book.author}") Debug.log(bookSource.bookSourceUrl, "┌获取分类") diff --git a/app/src/main/java/io/legado/app/model/webBook/BookList.kt b/app/src/main/java/io/legado/app/model/webBook/BookList.kt index 474ed653c..85e5e87c0 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookList.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookList.kt @@ -11,6 +11,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils.wordCountFormat +import io.legado.app.utils.htmlFormat import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive @@ -161,7 +162,7 @@ object BookList { Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}") if (!scope.isActive) throw CancellationException() Debug.log(bookSource.bookSourceUrl, "┌获取简介") - searchBook.intro = analyzeRule.getString(intro) + searchBook.intro = analyzeRule.getString(intro).htmlFormat() Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", true) if (!scope.isActive) throw CancellationException() Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") @@ -221,7 +222,7 @@ object BookList { Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}", log) if (!scope.isActive) throw CancellationException() Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) - searchBook.intro = analyzeRule.getString(ruleIntro) + searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat() Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", log, true) if (!scope.isActive) throw CancellationException() Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) diff --git a/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt b/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt new file mode 100644 index 000000000..1224979af --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt @@ -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() + + @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) + fun onSearchFinish() + fun onSearchCancel() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/WebBook.kt b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt similarity index 95% rename from app/src/main/java/io/legado/app/model/WebBook.kt rename to app/src/main/java/io/legado/app/model/webBook/WebBook.kt index cb1742132..22b75ed84 100644 --- a/app/src/main/java/io/legado/app/model/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt @@ -1,15 +1,12 @@ -package io.legado.app.model +package io.legado.app.model.webBook import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.model.webBook.BookChapterList -import io.legado.app.model.webBook.BookContent -import io.legado.app.model.webBook.BookInfo -import io.legado.app.model.webBook.BookList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -93,7 +90,8 @@ class WebBook(val bookSource: BookSource) { fun getBookInfo( book: Book, scope: CoroutineScope = Coroutine.DEFAULT, - context: CoroutineContext = Dispatchers.IO + context: CoroutineContext = Dispatchers.IO, + canReName: Boolean = true, ): Coroutine { return Coroutine.async(scope, context) { book.type = bookSource.bookSourceType @@ -109,7 +107,7 @@ class WebBook(val bookSource: BookSource) { ) analyzeUrl.getResponseAwait(bookSource.bookSourceUrl).body } - BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl) + BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl, canReName) book } } diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index dbfebd323..2fd69506e 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -336,6 +336,7 @@ abstract class BaseReadAloudService : BaseService(), abstract fun aloudServicePendingIntent(actionStr: String): PendingIntent? open fun nextChapter() { + ReadBook.upReadStartTime() if (!ReadBook.moveToNextChapter(true)) { stopSelf() } diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt index b21d4e36e..047e8d643 100644 --- a/app/src/main/java/io/legado/app/service/DownloadService.kt +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -16,7 +16,7 @@ import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.Download import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index c70c06388..4fe576dcf 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -7,8 +7,8 @@ import io.legado.app.constant.EventBus import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp import io.legado.app.help.coroutine.Coroutine -import io.legado.app.help.http.HttpHelper -import io.legado.app.help.http.api.HttpPostApi +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook import io.legado.app.utils.FileUtils import io.legado.app.utils.LogUtils @@ -17,7 +17,6 @@ import kotlinx.coroutines.isActive import java.io.File import java.io.FileDescriptor import java.io.FileInputStream -import java.net.URLEncoder class HttpReadAloudService : BaseReadAloudService(), MediaPlayer.OnPreparedListener, @@ -31,7 +30,7 @@ class HttpReadAloudService : BaseReadAloudService(), override fun onCreate() { super.onCreate() - ttsFolder = cacheDir.absolutePath + File.separator + "bdTts" + ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS" mediaPlayer.setOnErrorListener(this) mediaPlayer.setOnPreparedListener(this) mediaPlayer.setOnCompletionListener(this) @@ -72,19 +71,21 @@ class HttpReadAloudService : BaseReadAloudService(), FileUtils.deleteFile(ttsFolder) for (index in 0 until contentList.size) { if (isActive) { - val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com") - .create(HttpPostApi::class.java) - .postMapByteAsync( - "http://tts.baidu.com/text2audio", - getAudioBody(contentList[index]), mapOf() - ).body() - if (bytes != null && isActive) { - val file = getSpeakFile(index) - file.writeBytes(bytes) - if (index == nowSpeak) { - @Suppress("BlockingMethodInNonBlockingContext") - val fis = FileInputStream(file) - playAudio(fis.fd) + ReadAloud.httpTTS?.let { + AnalyzeUrl( + it.url, + speakText = contentList[index], + speakSpeed = AppConfig.ttsSpeechRate + ).getResponseBytes()?.let { bytes -> + if (isActive) { + val file = getSpeakFile(index) + file.writeBytes(bytes) + if (index == nowSpeak) { + @Suppress("BlockingMethodInNonBlockingContext") + val fis = FileInputStream(file) + playAudio(fis.fd) + } + } } } } else { @@ -113,31 +114,6 @@ class HttpReadAloudService : BaseReadAloudService(), return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") } - private fun getAudioBody(content: String): Map { - return mapOf( - Pair("tex", encodeTwo(content)), - Pair("spd", ((AppConfig.ttsSpeechRate + 5) / 10 + 4).toString()), - Pair("per", AppConfig.ttsSpeechPer), - Pair("cuid", "baidu_speech_demo"), - Pair("idx", "1"), - Pair("cod", "2"), - Pair("lan", "zh"), - Pair("ctp", "1"), - Pair("pdt", "1"), - Pair("vol", "5"), - Pair("pit", "5"), - Pair("_res_tag_", "audio") - ) - } - - private fun encodeTwo(content: String): String { - return try { - URLEncoder.encode(URLEncoder.encode(content, "UTF-8"), "UTF-8") - } catch (e: Exception) { - " " - } - } - override fun pauseReadAloud(pause: Boolean) { super.pauseReadAloud(pause) mediaPlayer.pause() diff --git a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt index 7c8d120b3..06bb6b5a9 100644 --- a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt +++ b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData import io.legado.app.constant.IntentAction import io.legado.app.constant.Status import io.legado.app.data.entities.Book -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.AudioPlayService object AudioPlay { diff --git a/app/src/main/java/io/legado/app/service/help/CheckSource.kt b/app/src/main/java/io/legado/app/service/help/CheckSource.kt index c387e526c..7ae99e667 100644 --- a/app/src/main/java/io/legado/app/service/help/CheckSource.kt +++ b/app/src/main/java/io/legado/app/service/help/CheckSource.kt @@ -7,7 +7,7 @@ import io.legado.app.R import io.legado.app.constant.IntentAction import io.legado.app.data.entities.BookSource import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.CheckSourceService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/io/legado/app/service/help/ReadAloud.kt b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt index b5c12a965..b25c270c3 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadAloud.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt @@ -5,22 +5,31 @@ import android.content.Intent import io.legado.app.App import io.legado.app.constant.IntentAction import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.HttpTTS import io.legado.app.service.BaseReadAloudService import io.legado.app.service.HttpReadAloudService import io.legado.app.service.TTSReadAloudService -import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefLong object ReadAloud { - var aloudClass: Class<*> = getReadAloudClass() + private var aloudClass: Class<*> = getReadAloudClass() + var httpTTS: HttpTTS? = null - fun getReadAloudClass(): Class<*> { - return if (App.INSTANCE.getPrefBoolean(PreferKey.readAloudOnLine)) { + private fun getReadAloudClass(): Class<*> { + val spId = App.INSTANCE.getPrefLong(PreferKey.speakEngine) + httpTTS = App.db.httpTTSDao().get(spId) + stop(App.INSTANCE) + return if (httpTTS != null) { HttpReadAloudService::class.java } else { TTSReadAloudService::class.java } } + fun upReadAloudClass() { + aloudClass = getReadAloudClass() + } + fun play( context: Context, title: String, diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index 2827ba5ee..25858c022 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -13,7 +13,7 @@ import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp import io.legado.app.help.IntentDataHelp import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.BaseReadAloudService import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.provider.ChapterProvider @@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.jetbrains.anko.getStackTraceString import org.jetbrains.anko.toast @@ -370,12 +371,20 @@ object ReadBook { } }.onError { it.printStackTrace() - App.INSTANCE.toast(it.localizedMessage ?: "ChapterProvider ERROR") + App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}") } } private val imageStyle get() = webBook?.bookSource?.ruleContent?.imageStyle + fun setCharset(charset: String) { + book?.let { + it.charset = charset + callBack?.loadChapterList(it) + } + saveRead() + } + fun saveRead() { Coroutine.async { book?.let { book -> @@ -392,6 +401,7 @@ object ReadBook { } interface CallBack { + fun loadChapterList(book: Book) fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) fun upView() fun pageChanged() diff --git a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt index b9e1b2c60..44a85a7fe 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt @@ -11,6 +11,7 @@ import io.legado.app.App import io.legado.app.R import io.legado.app.lib.dialogs.alert import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.applyTint import io.legado.app.utils.openUrl import io.legado.app.utils.sendToClip import io.legado.app.utils.toast @@ -34,7 +35,7 @@ class AboutFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.about) findPreference("check_update")?.summary = - "${getString(R.string.version)} ${App.INSTANCE.versionName}" + "${getString(R.string.version)} ${App.versionName}" } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -92,7 +93,7 @@ class AboutFragment : PreferenceFragmentCompat() { } } } - }.show() + }.show().applyTint() } private fun joinQQGroup(key: String): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt index 6ff52d229..5a1b3b75d 100644 --- a/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt @@ -8,7 +8,12 @@ import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.data.entities.ReadRecord +import io.legado.app.data.entities.ReadRecordShow +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.noButton +import io.legado.app.lib.dialogs.okButton +import io.legado.app.utils.applyTint +import io.legado.app.utils.visible import kotlinx.android.synthetic.main.activity_read_record.* import kotlinx.android.synthetic.main.item_read_record.* import kotlinx.android.synthetic.main.item_read_record.view.* @@ -16,6 +21,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jetbrains.anko.sdk27.listeners.onClick class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { @@ -27,7 +33,7 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { } private fun initView() { - tv_book_name.text = "总阅读时间" + tv_book_name.setText(R.string.all_read_time) recycler_view.layoutManager = LinearLayoutManager(this) adapter = RecordAdapter(this) recycler_view.adapter = adapter @@ -39,7 +45,7 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { withContext(Main) { tv_read_time.text = formatDuring(allTime) } - val readRecords = App.db.readRecordDao().all + val readRecords = App.db.readRecordDao().allShow withContext(Main) { adapter.setItems(readRecords) } @@ -47,17 +53,34 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { } inner class RecordAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_read_record) { + SimpleRecyclerAdapter(context, R.layout.item_read_record) { - override fun convert(holder: ItemViewHolder, item: ReadRecord, payloads: MutableList) { + override fun convert( + holder: ItemViewHolder, + item: ReadRecordShow, + payloads: MutableList + ) { holder.itemView.apply { tv_book_name.text = item.bookName tv_read_time.text = formatDuring(item.readTime) + iv_remove.visible() } } override fun registerListener(holder: ItemViewHolder) { - + holder.itemView.apply { + iv_remove.onClick { + alert(R.string.delete, R.string.sure_del) { + okButton { + getItem(holder.layoutPosition)?.let { + App.db.readRecordDao().deleteByName(it.bookName) + initData() + } + } + noButton() + }.show().applyTint() + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt index bfd63c362..6c5741414 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.association import android.content.Intent import android.os.Bundle -import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.Theme @@ -23,12 +22,12 @@ class FileAssociationActivity : VMBaseActivity( override fun onActivityCreated(savedInstanceState: Bundle?) { rotate_loading.show() - viewModel.errorLiveData.observe(this, Observer { + viewModel.errorLiveData.observe(this, { rotate_loading.hide() toast(it) finish() }) - viewModel.successLiveData.observe(this, Observer { + viewModel.successLiveData.observe(this, { rotate_loading.hide() startActivity(it) finish() diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt index 874bb9e5e..0b74690f2 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt @@ -9,7 +9,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment @@ -41,11 +40,11 @@ class ImportBookSourceActivity : VMBaseActivity( override fun onActivityCreated(savedInstanceState: Bundle?) { rotate_loading.show() - viewModel.errorLiveData.observe(this, Observer { + viewModel.errorLiveData.observe(this, { rotate_loading.hide() errorDialog(it) }) - viewModel.successLiveData.observe(this, Observer { + viewModel.successLiveData.observe(this, { rotate_loading.hide() if (it > 0) { successDialog() diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt index c5d757597..12ae447d1 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.association import android.os.Bundle -import androidx.lifecycle.Observer import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity @@ -26,11 +25,11 @@ class ImportReplaceRuleActivity : VMBaseActivity( override fun onActivityCreated(savedInstanceState: Bundle?) { rotate_loading.show() - viewModel.errorLiveData.observe(this, Observer { + viewModel.errorLiveData.observe(this, { rotate_loading.hide() errorDialog(it) }) - viewModel.successLiveData.observe(this, Observer { + viewModel.successLiveData.observe(this, { rotate_loading.hide() if (it.size > 0) { successDialog(it) diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt index 35e3ca284..8862f2dbe 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt @@ -9,7 +9,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment @@ -41,11 +40,11 @@ class ImportRssSourceActivity : VMBaseActivity( override fun onActivityCreated(savedInstanceState: Bundle?) { rotate_loading.show() - viewModel.errorLiveData.observe(this, Observer { + viewModel.errorLiveData.observe(this, { rotate_loading.hide() errorDialog(it) }) - viewModel.successLiveData.observe(this, Observer { + viewModel.successLiveData.observe(this, { rotate_loading.hide() if (it > 0) { successDialog() diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt b/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt index 2a339f46d..f1d2883fb 100644 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt +++ b/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.widget.SeekBar -import androidx.lifecycle.Observer import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions.bitmapTransform @@ -37,7 +36,7 @@ import org.jetbrains.anko.startActivityForResult class AudioPlayActivity : - VMBaseActivity(R.layout.activity_audio_play, theme = Theme.Dark), + VMBaseActivity(R.layout.activity_audio_play, toolBarTheme = Theme.Dark), ChangeSourceDialog.CallBack { override val viewModel: AudioPlayViewModel @@ -48,8 +47,8 @@ class AudioPlayActivity : override fun onActivityCreated(savedInstanceState: Bundle?) { title_bar.transparent() - AudioPlay.titleData.observe(this, Observer { title_bar.title = it }) - AudioPlay.coverData.observe(this, Observer { upCover(it) }) + AudioPlay.titleData.observe(this, { title_bar.title = it }) + AudioPlay.coverData.observe(this, { upCover(it) }) viewModel.initData(intent) initView() } diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt b/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt index 124fd710f..4c1a5a85b 100644 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt @@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.AudioPlay import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt index 491922cb1..ab37c5fe1 100644 --- a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt @@ -5,7 +5,6 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.widget.PopupMenu import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App @@ -94,7 +93,7 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi private fun initGroupData() { groupLiveData?.removeObservers(this) groupLiveData = App.db.bookGroupDao().liveDataAll() - groupLiveData?.observe(this, Observer { + groupLiveData?.observe(this, { groupList.clear() groupList.addAll(it) adapter.notifyDataSetChanged() @@ -112,7 +111,7 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() else -> App.db.bookDao().observeByGroup(groupId) } - booksLiveData?.observe(this, Observer { list -> + booksLiveData?.observe(this, { list -> val books = when (getPrefInt(PreferKey.bookshelfSort)) { 1 -> list.sortedByDescending { it.latestChapterTime } 2 -> list.sortedBy { it.name } diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt index 10a683227..4a898fed4 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt @@ -8,13 +8,12 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.Theme -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.dialog_change_source.* @@ -61,7 +60,7 @@ class ChangeCoverDialog : BaseDialogFragment(), } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) tool_bar.setTitle(R.string.change_cover_source) viewModel.initData(arguments) initMenu() @@ -70,7 +69,7 @@ class ChangeCoverDialog : BaseDialogFragment(), private fun initMenu() { tool_bar.inflateMenu(R.menu.change_cover) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) } @@ -83,7 +82,7 @@ class ChangeCoverDialog : BaseDialogFragment(), override fun observeLiveBus() { super.observeLiveBus() - viewModel.searchStateData.observe(viewLifecycleOwner, Observer { + viewModel.searchStateData.observe(viewLifecycleOwner, { refresh_progress_bar.isAutoLoading = it if (it) { stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp) @@ -92,7 +91,7 @@ class ChangeCoverDialog : BaseDialogFragment(), } tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) }) - viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer { + viewModel.searchBooksLiveData.observe(viewLifecycleOwner, { val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItems(it) diffResult.dispatchUpdatesTo(adapter) diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt index 0c91fe0f9..8a9e7ebe0 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt @@ -6,24 +6,31 @@ import androidx.lifecycle.MutableLiveData import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppPattern +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.Coroutine -import io.legado.app.model.WebBook +import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.model.webBook.WebBook import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors +import kotlin.math.min class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { - private var searchPool = - Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() + private val threadCount = AppConfig.threadCount + private var searchPool: ExecutorCoroutineDispatcher? = null var name: String = "" var author: String = "" - private var task: Coroutine<*>? = null + private var tasks = CompositeCoroutine() + private var bookSourceList = arrayListOf() val searchStateData = MutableLiveData() val searchBooksLiveData = MutableLiveData>() private val searchBooks = ArrayList() + @Volatile + private var searchIndex = -1 + fun initData(arguments: Bundle?) { arguments?.let { bundle -> bundle.getString("name")?.let { @@ -35,13 +42,18 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application } } + private fun initSearchPool() { + searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + searchIndex = -1 + } + fun loadDbSearchBook() { execute { App.db.searchBookDao().getEnableHasCover(name, author).let { searchBooks.addAll(it) if (it.size <= 1) { searchBooksLiveData.postValue(searchBooks) - search() + startSearch() } else { searchBooksLiveData.postValue(searchBooks) } @@ -49,50 +61,70 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application } } - fun search() { - task = execute { - val bookSourceList = App.db.bookSourceDao().allEnabled - for (item in bookSourceList) { - //task取消时自动取消 by (scope = this@execute) - WebBook(item).searchBook(name, scope = this@execute, context = searchPool) - .timeout(30000L) - .onSuccess(Dispatchers.IO) { - if (it.isNotEmpty()) { - val searchBook = it[0] - if (searchBook.name == name && searchBook.author == author - && !searchBook.coverUrl.isNullOrEmpty() - ) { - App.db.searchBookDao().insert(searchBook) - if (!searchBooks.contains(searchBook)) { - searchBooks.add(searchBook) - searchBooksLiveData.postValue(searchBooks) - } - } - } - } - } - }.onStart { + private fun startSearch() { + execute { + bookSourceList.clear() + bookSourceList.addAll(App.db.bookSourceDao().allEnabled) searchStateData.postValue(true) - }.onCancel { - searchStateData.postValue(false) + initSearchPool() + for (i in 0 until threadCount) { + search() + } } + } - task?.invokeOnCompletion { - searchStateData.postValue(false) + private fun search() { + synchronized(this) { + if (searchIndex >= bookSourceList.lastIndex) { + return + } + searchIndex++ + val source = bookSourceList[searchIndex] + val task = WebBook(source).searchBook(name, scope = this, context = searchPool!!) + .timeout(60000L) + .onSuccess(Dispatchers.IO) { + if (it.isNotEmpty()) { + val searchBook = it[0] + if (searchBook.name == name && searchBook.author == author + && !searchBook.coverUrl.isNullOrEmpty() + ) { + App.db.searchBookDao().insert(searchBook) + if (!searchBooks.contains(searchBook)) { + searchBooks.add(searchBook) + searchBooksLiveData.postValue(searchBooks) + } + } + } + } + .onFinally { + synchronized(this) { + if (searchIndex < bookSourceList.lastIndex) { + search() + } else { + searchIndex++ + } + if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, + threadCount) + ) { + searchStateData.postValue(false) + } + } + } + tasks.add(task) } } fun stopSearch() { - if (task?.isActive == true) { - task?.cancel() + if (tasks.isEmpty) { + startSearch() } else { - search() + tasks.clear() } } override fun onCleared() { super.onCleared() - searchPool.close() + searchPool?.close() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt index a4ca4244c..7ebb4c0db 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt @@ -9,17 +9,15 @@ import android.view.ViewGroup import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.PreferKey -import io.legado.app.constant.Theme import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint import io.legado.app.utils.getPrefBoolean @@ -69,7 +67,7 @@ class ChangeSourceDialog : BaseDialogFragment(), } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) viewModel.initData(arguments) showTitle() initMenu() @@ -86,7 +84,7 @@ class ChangeSourceDialog : BaseDialogFragment(), private fun initMenu() { tool_bar.inflateMenu(R.menu.change_source) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked = getPrefBoolean(PreferKey.changeSourceLoadToc) @@ -136,16 +134,16 @@ class ChangeSourceDialog : BaseDialogFragment(), } private fun initLiveData() { - viewModel.searchStateData.observe(viewLifecycleOwner, Observer { + viewModel.searchStateData.observe(viewLifecycleOwner, { refresh_progress_bar.isAutoLoading = it if (it) { stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp) } else { stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp) } - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) }) - viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer { + viewModel.searchBooksLiveData.observe(viewLifecycleOwner, { val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItems(it) diffResult.dispatchUpdatesTo(adapter) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt index da58200c5..6ce62b9f2 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt @@ -10,30 +10,36 @@ import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book +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.Coroutine -import io.legado.app.model.WebBook +import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.model.webBook.WebBook import io.legado.app.utils.getPrefBoolean import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import org.jetbrains.anko.debug +import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.Executors +import kotlin.math.min class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { - private var searchPool = - Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() + private val threadCount = AppConfig.threadCount + private var searchPool: ExecutorCoroutineDispatcher? = null val handler = Handler() val searchStateData = MutableLiveData() val searchBooksLiveData = MutableLiveData>() var name: String = "" var author: String = "" - private var task: Coroutine<*>? = null + private var tasks = CompositeCoroutine() private var screenKey: String = "" - private val searchBooks = hashSetOf() + private var bookSourceList = arrayListOf() + private val searchBooks = CopyOnWriteArraySet() private var postTime = 0L private val sendRunnable = Runnable { upAdapter() } - + @Volatile + private var searchIndex = -1 fun initData(arguments: Bundle?) { arguments?.let { bundle -> @@ -46,13 +52,18 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } } + private fun initSearchPool() { + searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + searchIndex = -1 + } + fun loadDbSearchBook() { execute { App.db.searchBookDao().getByNameAuthorEnable(name, author).let { searchBooks.addAll(it) if (it.size <= 1) { upAdapter() - search() + startSearch() } else { upAdapter() } @@ -83,38 +94,58 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio upAdapter() } - fun search() { - task = execute { - val bookSourceList = App.db.bookSourceDao().allEnabled - for (item in bookSourceList) { - //task取消时自动取消 by (scope = this@execute) - WebBook(item).searchBook(name, scope = this@execute, context = searchPool) - .timeout(30000L) - .onSuccess(IO) { - it.forEach { searchBook -> - if (searchBook.name == name && searchBook.author == author) { - if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { - if (searchBook.tocUrl.isEmpty()) { - loadBookInfo(searchBook.toBook()) - } else { - loadChapter(searchBook.toBook()) - } + private fun startSearch() { + execute { + bookSourceList.clear() + bookSourceList.addAll(App.db.bookSourceDao().allEnabled) + searchStateData.postValue(true) + initSearchPool() + for (i in 0 until threadCount) { + search() + } + } + } + + private fun search() { + synchronized(this) { + if (searchIndex >= bookSourceList.lastIndex) { + return + } + searchIndex++ + val source = bookSourceList[searchIndex] + val task = WebBook(source).searchBook(name, scope = this, context = searchPool!!) + .timeout(60000L) + .onSuccess(IO) { + it.forEach { searchBook -> + if (searchBook.name == name && searchBook.author == author) { + if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { + if (searchBook.tocUrl.isEmpty()) { + loadBookInfo(searchBook.toBook()) } else { - searchFinish(searchBook) + loadChapter(searchBook.toBook()) } - return@onSuccess + } else { + searchFinish(searchBook) } + return@forEach } } - } - }.onStart { - searchStateData.postValue(true) - }.onCancel { - searchStateData.postValue(false) - } - - task?.invokeOnCompletion { - searchStateData.postValue(false) + } + .onFinally { + synchronized(this) { + if (searchIndex < bookSourceList.lastIndex) { + search() + } else { + searchIndex++ + } + if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, + threadCount) + ) { + searchStateData.postValue(false) + } + } + } + tasks.add(task) } } @@ -166,16 +197,18 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } fun stopSearch() { - if (task?.isActive == true) { - task?.cancel() + if (tasks.isEmpty) { + startSearch() } else { - search() + tasks.clear() + searchPool?.close() + searchStateData.postValue(false) } } override fun onCleared() { super.onCleared() - searchPool.close() + searchPool?.close() } fun disableSource(searchBook: SearchBook) { diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt index ef47cd2c0..b24d438e8 100644 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt +++ b/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.View import android.widget.EditText import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import androidx.recyclerview.widget.LinearLayoutManager @@ -59,7 +58,7 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ LivePagedListBuilder( App.db.bookmarkDao().observeByBook(book.bookUrl, book.name, book.author), 20 ).build() - bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) + bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) }) } } @@ -74,7 +73,7 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ newText ), 20 ).build() - bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) + bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) }) } } diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt index 6ea580b31..5d4b3621b 100644 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt @@ -1,7 +1,7 @@ package io.legado.app.ui.book.chapterlist import android.content.Context -import android.widget.TextView +import android.view.View import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -21,8 +21,11 @@ class ChapterListAdapter(context: Context, val callback: Callback) : override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList) { with(holder.itemView) { + val isDur = callback.durChapterIndex() == item.index + val cached = callback.isLocalBook + || cacheFileNames.contains(BookHelp.formatChapterName(item)) if (payloads.isEmpty()) { - if (callback.durChapterIndex() == item.index) { + if (isDur) { tv_chapter_name.setTextColor(context.accentColor) } else { tv_chapter_name.setTextColor(context.getCompatColor(R.color.primaryText)) @@ -32,15 +35,9 @@ class ChapterListAdapter(context: Context, val callback: Callback) : tv_tag.text = item.tag tv_tag.visible() } - upHasCache( - tv_chapter_name, - cacheFileNames.contains(BookHelp.formatChapterName(item)) - ) + upHasCache(this, isDur, cached) } else { - upHasCache( - tv_chapter_name, - cacheFileNames.contains(BookHelp.formatChapterName(item)) - ) + upHasCache(this, isDur, cached) } } } @@ -53,11 +50,18 @@ class ChapterListAdapter(context: Context, val callback: Callback) : } } - private fun upHasCache(textView: TextView, contains: Boolean) { - textView.paint.isFakeBoldText = contains + private fun upHasCache(itemView: View, isDur: Boolean, cached: Boolean) = itemView.apply { + tv_chapter_name.paint.isFakeBoldText = cached + iv_checked.setImageResource(R.drawable.ic_outline_cloud_24) + iv_checked.visible(!cached) + if (isDur) { + iv_checked.setImageResource(R.drawable.ic_check) + iv_checked.visible() + } } interface Callback { + val isLocalBook: Boolean fun openChapter(bookChapter: BookChapter) fun durChapterIndex(): Int } diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt index 677d623e2..d4339bb23 100644 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt +++ b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment @@ -89,7 +88,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme private fun initDoc() { tocLiveData?.removeObservers(this@ChapterListFragment) tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl) - tocLiveData?.observe(viewLifecycleOwner, Observer { + tocLiveData?.observe(viewLifecycleOwner, { adapter.setItems(it) if (!scrollToDurChapter) { mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) @@ -124,12 +123,15 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme } else { tocLiveData?.removeObservers(this) tocLiveData = App.db.bookChapterDao().liveDataSearch(viewModel.bookUrl, newText) - tocLiveData?.observe(viewLifecycleOwner, Observer { + tocLiveData?.observe(viewLifecycleOwner, { adapter.setItems(it) }) } } + override val isLocalBook: Boolean + get() = viewModel.book?.isLocalBook() == true + override fun durChapterIndex(): Int { return durChapterIndex } diff --git a/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt b/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt index ecb9c6b67..d4c629381 100644 --- a/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import io.legado.app.App @@ -129,7 +128,7 @@ class DownloadActivity : VMBaseActivity(R.layout.activity_dow AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() else -> App.db.bookDao().observeByGroup(groupId) } - booksLiveData?.observe(this, Observer { list -> + booksLiveData?.observe(this, { list -> val booksDownload = list.filter { it.isOnLineTxt() } @@ -147,7 +146,7 @@ class DownloadActivity : VMBaseActivity(R.layout.activity_dow private fun initGroupData() { groupLiveData?.removeObservers(this) groupLiveData = App.db.bookGroupDao().liveDataAll() - groupLiveData?.observe(this, Observer { + groupLiveData?.observe(this, { groupList.clear() groupList.addAll(it) adapter.notifyDataSetChanged() diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt index dbba6b63e..a447bb9e1 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.book.explore import android.os.Bundle -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R @@ -27,7 +26,7 @@ class ExploreShowActivity : VMBaseActivity(R.layout.activi override fun onActivityCreated(savedInstanceState: Bundle?) { title_bar.title = intent.getStringExtra("exploreName") initRecyclerView() - viewModel.booksData.observe(this, Observer { upData(it) }) + viewModel.booksData.observe(this, { upData(it) }) viewModel.initData(intent) } diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index bd22b3968..9e7b8dd11 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -7,7 +7,7 @@ import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import kotlinx.coroutines.Dispatchers.IO class ExploreShowViewModel(application: Application) : BaseViewModel(application) { diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt index 8a02ba06f..d5efe8126 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt @@ -10,7 +10,6 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -20,7 +19,6 @@ 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.Theme import io.legado.app.data.entities.BookGroup import io.legado.app.help.AppConfig import io.legado.app.help.ItemTouchCallback @@ -30,7 +28,7 @@ import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.backgroundColor -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModel @@ -65,7 +63,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) tool_bar.title = getString(R.string.group_manage) initData() initMenu() @@ -79,7 +77,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener tv_ok.setTextColor(requireContext().accentColor) tv_ok.visible() tv_ok.onClick { dismiss() } - App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { + App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, { val diffResult = DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it)) adapter.setItems(it, diffResult) @@ -94,7 +92,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener tool_bar.setOnMenuItemClickListener(this) tool_bar.inflateMenu(R.menu.book_group_manage) tool_bar.menu.let { - it.applyTint(requireContext(), Theme.getTheme()) + it.applyTint(requireContext()) it.findItem(R.id.menu_group_all) .isChecked = AppConfig.bookGroupAllShow it.findItem(R.id.menu_group_local) diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt index a9bf52861..7a83ab140 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt @@ -11,7 +11,6 @@ import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -20,7 +19,6 @@ 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.Theme import io.legado.app.data.entities.BookGroup import io.legado.app.help.ItemTouchCallback import io.legado.app.lib.dialogs.alert @@ -29,7 +27,7 @@ import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.backgroundColor -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModel @@ -81,7 +79,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) callBack = activity as? CallBack arguments?.let { groupId = it.getInt("groupId") @@ -94,7 +92,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener private fun initView() { tool_bar.title = getString(R.string.group_select) tool_bar.inflateMenu(R.menu.book_group_manage) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) tool_bar.menu.setGroupVisible(R.id.menu_groups, false) adapter = GroupAdapter(requireContext()) @@ -114,7 +112,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } private fun initData() { - App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { + App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, { adapter.setItems(it) }) } diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index 9f61755eb..74c9c1acf 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -9,7 +9,6 @@ import android.view.Menu import android.view.MenuItem import android.widget.CheckBox import android.widget.LinearLayout -import androidx.lifecycle.Observer import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions.bitmapTransform @@ -25,6 +24,7 @@ import io.legado.app.help.IntentDataHelp import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changesource.ChangeSourceDialog @@ -35,10 +35,7 @@ import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.widget.image.CoverImageView -import io.legado.app.utils.dp -import io.legado.app.utils.getViewModel -import io.legado.app.utils.gone -import io.legado.app.utils.visible +import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_book_info.* import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.startActivity @@ -47,7 +44,7 @@ import org.jetbrains.anko.toast class BookInfoActivity : - VMBaseActivity(R.layout.activity_book_info, theme = Theme.Dark), + VMBaseActivity(R.layout.activity_book_info, toolBarTheme = Theme.Dark), GroupSelectDialog.CallBack, ChapterListAdapter.CallBack, ChangeSourceDialog.CallBack, @@ -60,14 +57,16 @@ class BookInfoActivity : override val viewModel: BookInfoViewModel get() = getViewModel(BookInfoViewModel::class.java) + @SuppressLint("PrivateResource") override fun onActivityCreated(savedInstanceState: Bundle?) { title_bar.transparent() arc_view.setBgColor(backgroundColor) ll_info.setBackgroundColor(backgroundColor) scroll_view.setBackgroundColor(backgroundColor) fl_action.setBackgroundColor(bottomBackground) - viewModel.bookData.observe(this, Observer { showBook(it) }) - viewModel.chapterListData.observe(this, Observer { upLoading(false, it) }) + tv_shelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground))) + viewModel.bookData.observe(this, { showBook(it) }) + viewModel.chapterListData.observe(this, { upLoading(false, it) }) viewModel.initData(intent) initOnClick() } @@ -97,7 +96,7 @@ class BookInfoActivity : if (it.isLocalBook()) { it.tocUrl = "" } - viewModel.loadBookInfo(it) + viewModel.loadBookInfo(it, false) } } R.id.menu_can_update -> { diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index 3a36170e4..71f046826 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -9,8 +9,8 @@ import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp -import io.legado.app.model.WebBook import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.ReadBook import kotlinx.coroutines.Dispatchers.IO @@ -49,15 +49,15 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } fun loadBookInfo( - book: Book, - changeDruChapterIndex: ((chapters: List) -> Unit)? = null + book: Book, canReName: Boolean = true, + changeDruChapterIndex: ((chapters: List) -> Unit)? = null, ) { execute { if (book.isLocalBook()) { loadChapter(book, changeDruChapterIndex) } else { App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getBookInfo(book, this) + WebBook(bookSource).getBookInfo(book, this, canReName = canReName) .onSuccess(IO) { bookData.postValue(book) if (inBookshelf) { @@ -138,7 +138,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } bookData.postValue(newBook) if (newBook.tocUrl.isEmpty()) { - loadBookInfo(newBook) { upChangeDurChapterIndex(newBook, it) } + loadBookInfo(newBook, false) { upChangeDurChapterIndex(newBook, it) } } else { loadChapter(newBook) { upChangeDurChapterIndex(newBook, it) } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt index 7315bdddc..06a149ca1 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.Book @@ -30,7 +29,7 @@ class BookInfoEditActivity : get() = getViewModel(BookInfoEditViewModel::class.java) override fun onActivityCreated(savedInstanceState: Bundle?) { - viewModel.bookData.observe(this, Observer { upView(it) }) + viewModel.bookData.observe(this, { upView(it) }) if (viewModel.bookData.value == null) { intent.getStringExtra("bookUrl")?.let { viewModel.loadBook(it) diff --git a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt index 994b7d8ca..86c5e5ecd 100644 --- a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt @@ -12,7 +12,6 @@ import android.view.View import androidx.appcompat.widget.PopupMenu import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R @@ -114,7 +113,7 @@ class ImportBookActivity : VMBaseActivity(R.layout.activity private fun initData() { localUriLiveData?.removeObservers(this) localUriLiveData = App.db.bookDao().observeLocalUri() - localUriLiveData?.observe(this, Observer { + localUriLiveData?.observe(this, { adapter.upBookHas(it) }) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/Help.kt b/app/src/main/java/io/legado/app/ui/book/read/Help.kt index ab86ed909..6a6a59912 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/Help.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/Help.kt @@ -14,15 +14,13 @@ import io.legado.app.R import io.legado.app.data.entities.Bookmark import io.legado.app.help.AppConfig import io.legado.app.help.ReadBookConfig -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton +import io.legado.app.lib.dialogs.* import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.backgroundColor import io.legado.app.service.help.Download import io.legado.app.service.help.ReadBook +import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.applyTint import io.legado.app.utils.requestInputMethod import kotlinx.android.synthetic.main.dialog_download_choice.view.* @@ -37,7 +35,7 @@ object Help { /** * 更新状态栏,导航栏 */ - fun upSystemUiVisibility(activity: Activity, toolBarHide: Boolean = true) { + fun upSystemUiVisibility(window: Window, toolBarHide: Boolean = true) { var flag = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_IMMERSIVE @@ -53,13 +51,13 @@ object Help { flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION } } - activity.window.decorView.systemUiVisibility = flag + window.decorView.systemUiVisibility = flag if (toolBarHide) { - ATH.setLightStatusBar(activity, ReadBookConfig.durConfig.statusIconDark()) + ATH.setLightStatusBar(window, ReadBookConfig.durConfig.statusIconDark()) } else { ATH.setLightStatusBarAuto( - activity, - ThemeStore.statusBarColor(activity, AppConfig.isTransparentStatusBar) + window, + ThemeStore.statusBarColor(App.INSTANCE, AppConfig.isTransparentStatusBar) ) } } @@ -181,4 +179,27 @@ object Help { noButton() }.show().applyTint().requestInputMethod() } + + @SuppressLint("InflateParams") + fun showCharsetConfig(context: Context) = with(context) { + val charsets = + arrayListOf("UTF-8", "GB2312", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") + alert(R.string.set_charset) { + var editText: AutoCompleteTextView? = null + customView { + layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { + editText = edit_view + edit_view.setFilterValues(charsets) + edit_view.setText(ReadBook.book?.charset) + } + } + okButton { + val text = editText?.text?.toString() + text?.let { + ReadBook.setCharset(it) + } + } + cancelButton() + }.show().applyTint() + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 7ad4008ac..3e1c82259 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -14,7 +14,6 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.core.view.get import androidx.core.view.isVisible import androidx.core.view.size -import androidx.lifecycle.Observer import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import io.legado.app.BuildConfig import io.legado.app.R @@ -108,11 +107,11 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo initView() upScreenTimeOut() ReadBook.callBack = this - ReadBook.titleDate.observe(this, Observer { + ReadBook.titleDate.observe(this) { title_bar.title = it upMenu() upView() - }) + } viewModel.initData(intent) } @@ -128,6 +127,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun onResume() { super.onResume() + ReadBook.readStartTime = System.currentTimeMillis() upSystemUiVisibility() timeBatteryReceiver = TimeBatteryReceiver.register(this) page_view.upTime() @@ -251,8 +251,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo R.id.menu_copy_text -> TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) R.id.menu_update_toc -> ReadBook.book?.let { - ReadBook.upMsg(getString(R.string.toc_updateing)) - viewModel.loadChapterList(it) + loadChapterList(it) } R.id.menu_enable_replace -> ReadBook.book?.let { it.useReplaceRule = !it.useReplaceRule @@ -275,6 +274,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo Pair("loginUrl", it.loginUrl) ) } + R.id.menu_set_charset -> Help.showCharsetConfig(this) } return super.onCompatOptionsItemSelected(item) } @@ -515,6 +515,11 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return false } + override fun loadChapterList(book: Book) { + ReadBook.upMsg(getString(R.string.toc_updateing)) + viewModel.loadChapterList(book) + } + /** * 内容加载完成 */ @@ -686,7 +691,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 更新状态栏,导航栏 */ override fun upSystemUiVisibility() { - Help.upSystemUiVisibility(this, !read_menu.isVisible) + Help.upSystemUiVisibility(window, !read_menu.isVisible) upNavigationBarColor() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index d7fb413f0..a23b56bb1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -10,8 +10,8 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp import io.legado.app.help.IntentDataHelp -import io.legado.app.model.WebBook import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook @@ -64,10 +64,17 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ReadBook.loadContent(resetPageOffset = true) } } else { - isInitFinish = true + if (ReadBook.durChapterIndex != book.durChapterIndex) { + ReadBook.durChapterIndex = book.durChapterIndex + ReadBook.durPageIndex = book.durChapterPos + ReadBook.prevTextChapter = null + ReadBook.curTextChapter = null + ReadBook.nextTextChapter = null + } ReadBook.book!!.group = book.group ReadBook.titleDate.postValue(book.name) ReadBook.upWebBook(book) + isInitFinish = true if (!book.isLocalBook() && ReadBook.webBook == null) { autoChangeSource(book.name, book.author) return @@ -82,6 +89,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } else { if (ReadBook.curTextChapter != null) { ReadBook.callBack?.upContent(resetPageOffset = false) + } else { + ReadBook.loadContent(resetPageOffset = true) } } } @@ -95,7 +104,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { if (book.isLocalBook()) { loadChapterList(book, changeDruChapterIndex) } else { - ReadBook.webBook?.getBookInfo(book, this) + ReadBook.webBook?.getBookInfo(book, this, canReName = false) ?.onSuccess { loadChapterList(book, changeDruChapterIndex) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt index 610fa3dde..11c2fed44 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt @@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.config import android.os.Bundle import android.util.DisplayMetrics -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.SeekBar import io.legado.app.R import io.legado.app.base.BaseDialogFragment @@ -14,7 +11,6 @@ import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud -import io.legado.app.ui.book.read.Help import io.legado.app.utils.ColorUtils import kotlinx.android.synthetic.main.dialog_auto_read.* import org.jetbrains.anko.sdk27.listeners.onClick @@ -26,10 +22,10 @@ class AutoReadDialog : BaseDialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) it.decorView.setPadding(0, 0, 0, 0) val attr = it.attributes diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index 99c951643..eaaf2f4d4 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -9,10 +9,7 @@ import android.graphics.Color import android.net.Uri import android.os.Bundle import android.util.DisplayMetrics -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.documentfile.provider.DocumentFile import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -47,10 +44,10 @@ class BgTextConfigDialog : BaseDialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) it.decorView.setPadding(0, 5, 0, 0) val attr = it.attributes @@ -102,7 +99,7 @@ class BgTextConfigDialog : BaseDialogFragment() { sw_dark_status_icon.onCheckedChange { buttonView, isChecked -> if (buttonView?.isPressed == true) { setStatusIconDark(isChecked) - activity?.let { + activity?.window?.let { Help.upSystemUiVisibility(it) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt index 2c2df5bbf..1e7aa8d1c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt @@ -4,10 +4,7 @@ import android.annotation.SuppressLint import android.content.SharedPreferences import android.os.Bundle import android.util.DisplayMetrics -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.LinearLayout import androidx.fragment.app.DialogFragment import androidx.preference.Preference @@ -30,10 +27,10 @@ class MoreConfigDialog : DialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) it.decorView.setPadding(0, 0, 0, 0) val attr = it.attributes diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt index a37d7e91b..dda13716a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt @@ -6,11 +6,11 @@ import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.WindowManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus import io.legado.app.help.ReadBookConfig -import io.legado.app.ui.book.read.Help import io.legado.app.utils.postEvent import kotlinx.android.synthetic.main.dialog_read_padding.* @@ -20,10 +20,10 @@ class PaddingConfigDialog : BaseDialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) val attr = it.attributes attr.dimAmount = 0.0f it.attributes = attr diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt index 16e7a39c2..3ae466454 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt @@ -10,16 +10,16 @@ import android.widget.LinearLayout import androidx.fragment.app.DialogFragment import androidx.preference.ListPreference import androidx.preference.Preference +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.help.AppConfig import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.backgroundColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud -import io.legado.app.ui.book.read.Help +import io.legado.app.utils.getPrefLong import io.legado.app.utils.postEvent class ReadAloudConfigDialog : DialogFragment() { @@ -29,11 +29,11 @@ class ReadAloudConfigDialog : DialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { - it.setBackgroundDrawableResource(R.color.transparent) + + it.setBackgroundDrawableResource(R.color.transparent) it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) } } @@ -62,11 +62,18 @@ class ReadAloudConfigDialog : DialogFragment() { class ReadAloudPreferenceFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { + private val speakEngineSummary: String + get() { + val eid = App.INSTANCE.getPrefLong(PreferKey.speakEngine) + val ht = App.db.httpTTSDao().get(eid) + return ht?.name ?: getString(R.string.local_tts) + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_aloud) upPreferenceSummary( - findPreference(PreferKey.ttsSpeechPer), - AppConfig.ttsSpeechPer + findPreference(PreferKey.speakEngine), + speakEngineSummary ) } @@ -85,6 +92,14 @@ class ReadAloudConfigDialog : DialogFragment() { super.onPause() } + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { + PreferKey.speakEngine -> + SpeakEngineDialog().show(childFragmentManager, "speakEngine") + } + return super.onPreferenceTreeClick(preference) + } + override fun onSharedPreferenceChanged( sharedPreferences: SharedPreferences?, key: String? @@ -95,16 +110,9 @@ class ReadAloudConfigDialog : DialogFragment() { postEvent(EventBus.MEDIA_BUTTON, false) } } - PreferKey.readAloudOnLine -> { - ReadAloud.stop(requireContext()) - ReadAloud.aloudClass = ReadAloud.getReadAloudClass() - } - PreferKey.ttsSpeechPer -> { - upPreferenceSummary( - findPreference(PreferKey.ttsSpeechPer), - AppConfig.ttsSpeechPer - ) - ReadAloud.upTtsSpeechRate(requireContext()) + PreferKey.speakEngine -> { + upPreferenceSummary(findPreference(key), speakEngineSummary) + ReadAloud.upReadAloudClass() } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt index 8c5c4e6a9..7fcd0f26d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt @@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.config import android.os.Bundle import android.util.DisplayMetrics -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.SeekBar import io.legado.app.R import io.legado.app.base.BaseDialogFragment @@ -16,7 +13,6 @@ import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook -import io.legado.app.ui.book.read.Help import io.legado.app.utils.ColorUtils import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.observeEvent @@ -31,10 +27,10 @@ class ReadAloudDialog : BaseDialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) it.decorView.setPadding(0, 0, 0, 0) val attr = it.attributes diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt index 50824ff50..77b789b24 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -4,16 +4,12 @@ import android.annotation.SuppressLint import android.content.DialogInterface import android.os.Bundle import android.util.DisplayMetrics -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.core.view.get import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.help.AppConfig import io.legado.app.help.ReadBookConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector @@ -21,7 +17,6 @@ import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.utils.* @@ -40,10 +35,10 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) it.decorView.setPadding(0, 0, 0, 0) val attr = it.attributes @@ -209,9 +204,6 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { upBg() postEvent(EventBus.UP_CONFIG, true) } - if (AppConfig.isEInkMode) { - toast(R.string.e_ink_change_bg) - } } private fun showBgTextConfig(index: Int): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt new file mode 100644 index 000000000..6ee1d5bda --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt @@ -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>? = 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(context, R.layout.item_http_tts) { + + override fun convert(holder: ItemViewHolder, item: HttpTTS, payloads: MutableList) { + 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) + } + } + } + } + + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt new file mode 100644 index 000000000..490eb6efd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt @@ -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(json)?.let { + App.db.txtTocRule().insert(*it.toTypedArray()) + } + } + }.onSuccess { + finally("导入成功") + }.onError { + finally("导入失败") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt index 65bcfc7b6..2a6f414cb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt @@ -22,19 +22,19 @@ class TipConfigDialog : BaseDialogFragment() { super.onStart() val dm = DisplayMetrics() activity?.let { - Help.upSystemUiVisibility(it) it.windowManager?.defaultDisplay?.getMetrics(dm) } - dialog?.window?.setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) + dialog?.window?.let { + Help.upSystemUiVisibility(it) + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT) + } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { return inflater.inflate(R.layout.dialog_tip_config, container) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt index a91e8cde4..4603a0d33 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt @@ -11,7 +11,6 @@ import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -21,7 +20,6 @@ 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.Theme import io.legado.app.data.entities.TxtTocRule import io.legado.app.help.ItemTouchCallback import io.legado.app.lib.dialogs.alert @@ -29,7 +27,7 @@ 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.backgroundColor -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* @@ -68,11 +66,11 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) durRegex = arguments?.getString("tocRegex") tool_bar.setTitle(R.string.txt_toc_regex) tool_bar.inflateMenu(R.menu.txt_toc_regex) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) initView() initData() @@ -105,7 +103,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { private fun initData() { tocRegexLiveData?.removeObservers(viewLifecycleOwner) tocRegexLiveData = App.db.txtTocRule().observeAll() - tocRegexLiveData?.observe(viewLifecycleOwner, Observer { tocRules -> + tocRegexLiveData?.observe(viewLifecycleOwner, { tocRules -> initSelectedName(tocRules) adapter.setItems(tocRules) }) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index 3dbc0a5e1..849c69b84 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -7,6 +7,7 @@ import android.text.StaticLayout import android.text.TextPaint import android.text.TextUtils import io.legado.app.App +import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter @@ -18,7 +19,6 @@ import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.utils.* import java.util.* -import java.util.regex.Pattern @Suppress("DEPRECATION") @@ -38,8 +38,6 @@ object ChapterProvider { var typeface: Typeface = Typeface.SANS_SERIF lateinit var titlePaint: TextPaint lateinit var contentPaint: TextPaint - private val srcPattern = - Pattern.compile("", Pattern.CASE_INSENSITIVE) init { upStyle() @@ -62,7 +60,7 @@ object ChapterProvider { var durY = 0f textPages.add(TextPage()) contents.forEachIndexed { index, text -> - val matcher = srcPattern.matcher(text) + val matcher = AppPattern.imgPattern.matcher(text) if (matcher.find()) { var src = matcher.group(1) if (!book.isEpub()) { diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index acf63099b..2870818c2 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -7,13 +7,13 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.appcompat.widget.SearchView import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.flexbox.FlexboxLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook @@ -176,17 +176,17 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } private fun initLiveData() { - App.db.bookSourceDao().liveGroupEnabled().observe(this, Observer { + App.db.bookSourceDao().liveGroupEnabled().observe(this, { groups.clear() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupMenu() }) - viewModel.searchBookLiveData.observe(this, Observer { + viewModel.searchBookLiveData.observe(this, { upSearchItems(it) }) - viewModel.isSearchLiveData.observe(this, Observer { + viewModel.isSearchLiveData.observe(this, { if (it) { startSearch() } else { @@ -254,7 +254,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se rv_bookshelf_search.gone() } else { bookData = App.db.bookDao().liveDataSearch(key) - bookData?.observe(this, Observer { + bookData?.observe(this, { if (it.isEmpty()) { tv_book_show.gone() rv_bookshelf_search.gone() @@ -272,7 +272,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } else { App.db.searchKeywordDao().liveDataSearch(key) } - historyData?.observe(this, Observer { + historyData?.observe(this, { historyKeyAdapter.setItems(it) if (it.isEmpty()) { tv_clear_history.invisible() diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index 318937697..179b2cd9b 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel import io.legado.app.constant.PreferKey import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword -import io.legado.app.model.SearchBookModel +import io.legado.app.model.webBook.SearchBookModel import io.legado.app.utils.getPrefBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive @@ -30,13 +30,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application), * 开始搜索 */ fun search(key: String) { - if (searchKey != key && key.isNotEmpty()) { + if ((searchKey == key) || key.isNotEmpty()) { searchBookModel.cancelSearch() searchBooks.clear() searchBookLiveData.postValue(searchBooks) searchID = System.currentTimeMillis() searchKey = key } + if (searchKey.isEmpty()) { + return + } searchBookModel.search(searchID, searchKey) } @@ -54,6 +57,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application), override fun onSearchStart() { isSearchLiveData.postValue(true) + isLoading = true } override fun onSearchSuccess(searchBooks: ArrayList) { @@ -110,7 +114,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application), && item.author == searchBook.author ) { hasSame = true - searchBook.addOrigin(item.bookUrl) + searchBook.addOrigin(item.origin) break } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt index b17ec5e9f..c7755401d 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt @@ -4,7 +4,7 @@ import android.app.Application import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.model.Debug -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook class BookSourceDebugModel(application: Application) : BaseViewModel(application), Debug.Callback { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index da57654f4..b673fe729 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -11,13 +11,13 @@ import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern import io.legado.app.data.entities.BookSource import io.legado.app.help.IntentDataHelp import io.legado.app.help.ItemTouchCallback @@ -31,6 +31,7 @@ import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FilePicker import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* @@ -137,6 +138,13 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity val itemTouchCallback = ItemTouchCallback() itemTouchCallback.onItemTouchCallbackListener = adapter itemTouchCallback.isCanDrag = true + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.initDragSelectTouchHelperCallback()).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(recycler_view) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + + // Note: need judge selection first, so add ItemTouchHelper after it. ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) } @@ -164,7 +172,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity App.db.bookSourceDao().liveDataSearch("%$searchKey%") } } - bookSourceLiveDate?.observe(this, Observer { data -> + bookSourceLiveDate?.observe(this, { data -> val sourceList = when (sort) { 1 -> data.sortedBy { it.weight } 2 -> data.sortedBy { it.bookSourceName } @@ -179,10 +187,10 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity } private fun initLiveDataGroup() { - App.db.bookSourceDao().liveGroup().observe(this, Observer { + App.db.bookSourceDao().liveGroup().observe(this, { groups.clear() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupMenu() }) diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt index a035341ee..7bdb34018 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt @@ -14,6 +14,7 @@ import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.BookSource import io.legado.app.help.ItemTouchCallback.OnItemTouchCallbackListener import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.utils.invisible import io.legado.app.utils.visible import kotlinx.android.synthetic.main.item_book_source.view.* @@ -193,6 +194,31 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : } } + fun initDragSelectTouchHelperCallback(): DragSelectTouchHelper.Callback { + return object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): BookSource { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + return true + } + return false + } + } + } + interface CallBack { fun del(bookSource: BookSource) fun edit(bookSource: BookSource) diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index 8038cf0db..29be62163 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -5,6 +5,7 @@ import android.text.TextUtils import androidx.documentfile.provider.DocumentFile import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppPattern import io.legado.app.data.entities.BookSource import io.legado.app.utils.FileUtils import io.legado.app.utils.GSON @@ -94,41 +95,41 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } fun selectionAddToGroups(sources: List, groups: String) { - execute { - val list = arrayListOf() - sources.forEach { - val newGroupList = arrayListOf() - it.bookSourceGroup?.splitNotBlank(",", ";")?.forEach { - newGroupList.add(it) - } - groups.splitNotBlank(",", ";", ",").forEach { - newGroupList.add(it) - } - val lh = LinkedHashSet(newGroupList) - val newGroup = ArrayList(lh).joinToString(separator = ",") - list.add(it.copy(bookSourceGroup = newGroup)) + execute { + val list = arrayListOf() + sources.forEach { source -> + val newGroupList = arrayListOf() + source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.forEach { + newGroupList.add(it) } - App.db.bookSourceDao().update(*list.toTypedArray()) + groups.splitNotBlank(",", ";", ",").forEach { + newGroupList.add(it) + } + val lh = LinkedHashSet(newGroupList) + val newGroup = ArrayList(lh).joinToString(separator = ",") + list.add(source.copy(bookSourceGroup = newGroup)) } + App.db.bookSourceDao().update(*list.toTypedArray()) + } } fun selectionRemoveFromGroups(sources: List, groups: String) { - execute { - val list = arrayListOf() - sources.forEach { - val newGroupList = arrayListOf() - it.bookSourceGroup?.splitNotBlank(",", ";")?.forEach { - newGroupList.add(it) - } - groups.splitNotBlank(",", ";", ",").forEach { - newGroupList.remove(it) - } - val lh = LinkedHashSet(newGroupList) - val newGroup = ArrayList(lh).joinToString(separator = ",") - list.add(it.copy(bookSourceGroup = newGroup)) + execute { + val list = arrayListOf() + sources.forEach { source -> + val newGroupList = arrayListOf() + source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.forEach { + newGroupList.add(it) } - App.db.bookSourceDao().update(*list.toTypedArray()) + groups.splitNotBlank(",", ";", ",").forEach { + newGroupList.remove(it) + } + val lh = LinkedHashSet(newGroupList) + val newGroup = ArrayList(lh).joinToString(separator = ",") + list.add(source.copy(bookSourceGroup = newGroup)) } + App.db.bookSourceDao().update(*list.toTypedArray()) + } } fun delSelection(sources: List) { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt index a926dbe55..5c7136691 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -11,18 +11,18 @@ import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.Theme +import io.legado.app.constant.AppPattern import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModelOfActivity @@ -56,22 +56,23 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setBackgroundColor(backgroundColor) - initData() - } - - private fun initData() { + tool_bar.setBackgroundColor(primaryColor) tool_bar.title = getString(R.string.group_manage) tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.addItemDecoration(VerticalDivider(requireContext())) recycler_view.adapter = adapter - App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { + initData() + } + + private fun initData() { + App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, { val groups = linkedSetOf() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } adapter.setItems(groups.toList()) }) diff --git a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt index 391a76ddf..39939e8cf 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt @@ -19,6 +19,7 @@ import io.legado.app.help.storage.WebDavHelp import io.legado.app.ui.filechooser.FilePicker import io.legado.app.utils.getPrefString import io.legado.app.utils.isContentPath +import io.legado.app.utils.longToast import io.legado.app.utils.toast import kotlinx.coroutines.Dispatchers.Main import org.jetbrains.anko.toast @@ -77,24 +78,24 @@ object BackupRestoreUi { fun restore(fragment: Fragment) { Coroutine.async(context = Main) { - val restoreFromWebDav = WebDavHelp.showRestoreDialog(fragment.requireContext()) - if (!restoreFromWebDav) { - val backupPath = fragment.getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - if (backupPath.isContentPath()) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) - if (doc?.canWrite() == true) { - Restore.restore(fragment.requireContext(), backupPath) - } else { - selectBackupFolder(fragment, restoreSelectRequestCode) - } + WebDavHelp.showRestoreDialog(fragment.requireContext()) + }.onError { + fragment.longToast("WebDavError:${it.localizedMessage},将从本地备份恢复。") + val backupPath = fragment.getPrefString(PreferKey.backupPath) + if (backupPath?.isNotEmpty() == true) { + if (backupPath.isContentPath()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) + if (doc?.canWrite() == true) { + Restore.restore(fragment.requireContext(), backupPath) } else { - restoreUsePermission(fragment, backupPath) + selectBackupFolder(fragment, restoreSelectRequestCode) } } else { - selectBackupFolder(fragment, restoreSelectRequestCode) + restoreUsePermission(fragment, backupPath) } + } else { + selectBackupFolder(fragment, restoreSelectRequestCode) } } } diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index bf130b50e..c0efd5698 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -20,6 +20,9 @@ import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.noButton +import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.receiver.SharedReceiverActivity @@ -79,11 +82,7 @@ class OtherConfigFragment : BasePreferenceFragment(), .show { putPrefInt(PreferKey.webPort, it) } - PreferKey.cleanCache -> { - BookHelp.clearCache() - FileUtils.deleteFile(requireActivity().cacheDir.absolutePath) - toast(R.string.clear_cache_success) - } + PreferKey.cleanCache -> clearCache() PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { selectDefaultCover() } else { @@ -145,6 +144,18 @@ class OtherConfigFragment : BasePreferenceFragment(), } } + private fun clearCache() { + requireContext().alert(titleResource = R.string.clear_cache, + messageResource = R.string.sure_del) { + okButton { + BookHelp.clearCache() + FileUtils.deleteFile(requireActivity().cacheDir.absolutePath) + toast(R.string.clear_cache_success) + } + noButton() + }.show().applyTint() + } + private fun selectDefaultCover() { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.addCategory(Intent.CATEGORY_OPENABLE) diff --git a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt index f1e6a7bc9..fb2e5ac19 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt @@ -12,7 +12,7 @@ import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R -import io.legado.app.constant.Theme +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.filechooser.adapter.FileAdapter import io.legado.app.ui.filechooser.adapter.PathAdapter import io.legado.app.ui.widget.recycler.VerticalDivider @@ -93,6 +93,7 @@ class FileChooserDialog : DialogFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + tool_bar.setBackgroundColor(primaryColor) view.setBackgroundResource(R.color.background_card) arguments?.let { requestCode = it.getInt("requestCode") @@ -129,7 +130,7 @@ class FileChooserDialog : DialogFragment(), tool_bar.menu.add(menuTitle) } } - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) } diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 0c02d5abd..7d6c71134 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -62,7 +62,7 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), //自动更新书籍 if (AppConfig.autoRefreshBook) { view_pager_main.postDelayed({ - viewModel.upChapterList() + viewModel.upAllBookToc() }, 1000) } view_pager_main.postDelayed({ @@ -93,8 +93,8 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), } private fun upVersion() { - if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) { - putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode) + if (getPrefInt(PreferKey.versionCode) != App.versionCode) { + putPrefInt(PreferKey.versionCode, App.versionCode) if (!BuildConfig.DEBUG) { val log = String(assets.open("updateLog.md").readBytes()) TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000, true) @@ -168,7 +168,7 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), } } - private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : + private inner class TabFragmentPageAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItemPosition(`object`: Any): Int { diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 06fd049d3..c9fc9066a 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -8,22 +8,28 @@ import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.data.entities.RssSource import io.legado.app.help.AppConfig +import io.legado.app.help.DefaultValueHelp import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.Restore -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.utils.FileUtils import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.delay +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.Executors class MainViewModel(application: Application) : BaseViewModel(application) { - private var upTocPool = - Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() - val updateList = hashSetOf() + private var threadCount = AppConfig.threadCount + private var upTocPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + val updateList = CopyOnWriteArraySet() + private val bookMap = ConcurrentHashMap() + + @Volatile + private var usePoolCount = 0 override fun onCleared() { super.onCleared() @@ -31,52 +37,81 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } fun upPool() { + threadCount = AppConfig.threadCount upTocPool.close() - upTocPool = Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() + upTocPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() } - fun upChapterList() { + fun upAllBookToc() { execute { - upChapterList(App.db.bookDao().hasUpdateBooks) + upToc(App.db.bookDao().hasUpdateBooks) } } - fun upChapterList(books: List) { + fun upToc(books: List) { execute { books.filter { it.origin != BookType.local && it.canUpdate - }.forEach { book -> - if (!updateList.contains(book.bookUrl)) { + }.forEach { + bookMap[it.bookUrl] = it + } + for (i in 0 until threadCount) { + if (usePoolCount < threadCount) { + usePoolCount++ + updateToc() + } + } + } + } + + private fun updateToc() { + synchronized(this) { + bookMap.forEach { bookEntry -> + if (!updateList.contains(bookEntry.key)) { + val book = bookEntry.value + synchronized(this) { + updateList.add(book.bookUrl) + postEvent(EventBus.UP_BOOK, book.bookUrl) + } App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - synchronized(this) { - updateList.add(book.bookUrl) - postEvent(EventBus.UP_BOOK, book.bookUrl) - } WebBook(bookSource).getChapterList(book, context = upTocPool) .timeout(300000) .onSuccess(IO) { - synchronized(this) { - updateList.remove(book.bookUrl) - postEvent(EventBus.UP_BOOK, book.bookUrl) - } App.db.bookDao().update(book) App.db.bookChapterDao().delByBook(book.bookUrl) App.db.bookChapterDao().insert(*it.toTypedArray()) } .onError { + it.printStackTrace() + } + .onFinally { synchronized(this) { + bookMap.remove(bookEntry.key) updateList.remove(book.bookUrl) postEvent(EventBus.UP_BOOK, book.bookUrl) + upNext() } - it.printStackTrace() } + } ?: synchronized(this) { + bookMap.remove(bookEntry.key) + updateList.remove(book.bookUrl) + postEvent(EventBus.UP_BOOK, book.bookUrl) + upNext() } + return } - delay(50) } } } + private fun upNext() { + if (bookMap.size > updateList.size) { + updateToc() + } else { + usePoolCount-- + } + } + fun initRss() { execute { val url = "https://gitee.com/alanskycn/yuedu/raw/master/JS/RSS/rssSource" @@ -97,6 +132,9 @@ class MainViewModel(application: Application) : BaseViewModel(application) { fun postLoad() { execute { FileUtils.deleteFile(FileUtils.getPath(context.cacheDir, "Fonts")) + if (App.db.httpTTSDao().count == 0) { + DefaultValueHelp.initHttpTTS() + } } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt index 1466f92f0..4444f33d7 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt @@ -11,7 +11,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import com.google.android.material.tabs.TabLayout import io.legado.app.App import io.legado.app.R @@ -79,7 +78,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b val group = bookGroups[tab_layout.selectedTabPosition] val fragment = fragmentMap[group.groupId] fragment?.getBooks()?.let { - activityViewModel.upChapterList(it) + activityViewModel.upToc(it) } } R.id.menu_bookshelf_layout -> configBookshelf() @@ -114,7 +113,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b private fun initBookGroupData() { bookGroupLiveData?.removeObservers(viewLifecycleOwner) bookGroupLiveData = App.db.bookGroupDao().liveDataAll() - bookGroupLiveData?.observe(viewLifecycleOwner, Observer { + bookGroupLiveData?.observe(viewLifecycleOwner, { viewModel.checkGroup(it) launch { synchronized(this) { @@ -152,7 +151,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b }) noGroupLiveData?.removeObservers(viewLifecycleOwner) noGroupLiveData = App.db.bookDao().observeNoGroupSize() - noGroupLiveData?.observe(viewLifecycleOwner, Observer { + noGroupLiveData?.observe(viewLifecycleOwner, { if (it > 0 && !showGroupNone && AppConfig.bookGroupNoneShow) { showGroupNone = true upGroup() @@ -270,7 +269,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b fragmentMap[selectedGroup?.groupId]?.gotoTop() } - private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : + private inner class TabFragmentPageAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getPageTitle(position: Int): CharSequence? { diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index dda342015..ae1287842 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -7,7 +7,7 @@ import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.Dispatchers.IO diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt index f0731f1ed..ebbe310e8 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt @@ -3,7 +3,6 @@ package io.legado.app.ui.main.bookshelf.books import android.os.Bundle import android.view.View import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -67,7 +66,7 @@ class BooksFragment : BaseFragment(R.layout.fragment_books), refresh_layout.setColorSchemeColors(accentColor) refresh_layout.setOnRefreshListener { refresh_layout.isRefreshing = false - activityViewModel.upChapterList(booksAdapter.getItems()) + activityViewModel.upToc(booksAdapter.getItems()) } val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) if (bookshelfLayout == 0) { @@ -106,7 +105,7 @@ class BooksFragment : BaseFragment(R.layout.fragment_books), AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() else -> App.db.bookDao().observeByGroup(groupId) } - bookshelfLiveData?.observe(this, Observer { list -> + bookshelfLiveData?.observe(this, { list -> val books = when (getPrefInt(PreferKey.bookshelfSort)) { 1 -> list.sortedByDescending { it.latestChapterTime } 2 -> list.sortedBy { it.name } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index c8f93fc64..6aa870ee7 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -7,13 +7,13 @@ import android.view.SubMenu import android.view.View import androidx.appcompat.widget.SearchView import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppPattern import io.legado.app.data.entities.BookSource import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor @@ -93,10 +93,10 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ private fun initGroupData() { liveGroup?.removeObservers(viewLifecycleOwner) liveGroup = App.db.bookSourceDao().liveGroupExplore() - liveGroup?.observe(viewLifecycleOwner, Observer { + liveGroup?.observe(viewLifecycleOwner, { groups.clear() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupsMenu() }) @@ -109,7 +109,7 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ } else { App.db.bookSourceDao().liveExplore("%$key%") } - liveExplore?.observe(viewLifecycleOwner, Observer { + liveExplore?.observe(viewLifecycleOwner, { val diffResult = DiffUtil .calculateDiff(ExploreDiffCallBack(ArrayList(adapter.getItems()), it)) adapter.setItems(it) diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index 334ce9e64..82ccf867f 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.App import io.legado.app.R @@ -56,7 +55,7 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), } private fun initData() { - App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, Observer { + App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, { if (it.isEmpty()) { getViewModelOfActivity(MainViewModel::class.java).initRss() } diff --git a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt index c83d1f4b7..bf9cb5409 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt @@ -11,19 +11,18 @@ import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.Theme +import io.legado.app.constant.AppPattern import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.backgroundColor -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModelOfActivity @@ -57,23 +56,23 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setBackgroundColor(backgroundColor) - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) initData() } private fun initData() { tool_bar.title = getString(R.string.group_manage) tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.addItemDecoration(VerticalDivider(requireContext())) recycler_view.adapter = adapter - App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, Observer { + App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, { val groups = linkedSetOf() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } adapter.setItems(groups.toList()) }) diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt index 554bf635b..52fec9244 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt @@ -11,13 +11,13 @@ import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern import io.legado.app.data.entities.ReplaceRule import io.legado.app.help.BookHelp import io.legado.app.help.IntentDataHelp @@ -133,7 +133,7 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi } else { App.db.replaceRuleDao().liveDataSearch(key) } - replaceRuleLiveData?.observe(this, Observer { + replaceRuleLiveData?.observe(this, { if (dataInit) { setResult(Activity.RESULT_OK) } @@ -146,10 +146,10 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi } private fun observeGroupData() { - App.db.replaceRuleDao().liveGroup().observe(this, Observer { + App.db.replaceRuleDao().liveGroup().observe(this, { groups.clear() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupMenu() }) diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt index 6b207cd0b..4bbfb44be 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt @@ -11,13 +11,11 @@ import android.widget.EditText import android.widget.PopupWindow import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.AppConst -import io.legado.app.constant.Theme import io.legado.app.data.entities.ReplaceRule -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.utils.applyTint import io.legado.app.utils.getViewModel @@ -67,12 +65,12 @@ class ReplaceEditDialog : BaseDialogFragment(), } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(bottomBackground) + tool_bar.setBackgroundColor(primaryColor) mSoftKeyboardTool = KeyboardToolPop(requireContext(), AppConst.keyboardToolChars, this) tool_bar.inflateMenu(R.menu.replace_edit) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) - viewModel.replaceRuleData.observe(viewLifecycleOwner, Observer { + viewModel.replaceRuleData.observe(viewLifecycleOwner, { upReplaceView(it) }) arguments?.let { diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt index f2329cb84..f9b684592 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt @@ -3,7 +3,6 @@ package io.legado.app.ui.rss.article import android.os.Bundle import android.view.View import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -87,7 +86,7 @@ class RssArticlesFragment : VMBaseFragment(R.layout.fragme activityViewModel.url?.let { rssArticlesData?.removeObservers(this) rssArticlesData = App.db.rssArticleDao().liveByOriginSort(it, viewModel.sortName) - rssArticlesData?.observe(viewLifecycleOwner, Observer { list -> + rssArticlesData?.observe(viewLifecycleOwner, { list -> adapter.setItems(list) }) } @@ -104,7 +103,7 @@ class RssArticlesFragment : VMBaseFragment(R.layout.fragme } override fun observeLiveBus() { - viewModel.loadFinally.observe(viewLifecycleOwner, Observer { + viewModel.loadFinally.observe(viewLifecycleOwner, { refresh_recycler_view.stopLoading() if (it) { loadMoreView.startLoad() diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt index 8ab79c861..44f125ef2 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -7,7 +7,7 @@ import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource -import io.legado.app.model.Rss +import io.legado.app.model.rss.Rss import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt index 41fad7292..307374f9f 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt @@ -8,7 +8,6 @@ import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter -import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity @@ -30,7 +29,7 @@ class RssSortActivity : VMBaseActivity(R.layout.activity_rss_a adapter = TabFragmentPageAdapter(supportFragmentManager) tab_layout.setupWithViewPager(view_pager) view_pager.adapter = adapter - viewModel.titleLiveData.observe(this, Observer { + viewModel.titleLiveData.observe(this, { title_bar.title = it }) viewModel.initData(intent) { @@ -85,7 +84,7 @@ class RssSortActivity : VMBaseActivity(R.layout.activity_rss_a } } - private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : + private inner class TabFragmentPageAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItemPosition(`object`: Any): Int { diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt index f50329296..09f6d1549 100644 --- a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.rss.favorites import android.os.Bundle import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R @@ -37,7 +36,7 @@ class RssFavoritesActivity : BaseActivity(R.layout.activity_rss_favorites), private fun initData() { liveData?.removeObservers(this) liveData = App.db.rssStarDao().liveAll() - liveData?.observe(this, Observer { + liveData?.observe(this, { adapter.setItems(it) }) } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index e11293cc4..908afa311 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.* import android.webkit.* import androidx.core.view.size -import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.lib.theme.DrawableUtils @@ -155,7 +154,7 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r @SuppressLint("SetJavaScriptEnabled") private fun initLiveData() { - viewModel.contentLiveData.observe(this, Observer { content -> + viewModel.contentLiveData.observe(this, { content -> viewModel.rssArticle?.let { upJavaScriptEnable() val url = NetworkUtils.getAbsoluteURL(it.origin, it.link) @@ -179,7 +178,7 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r } } }) - viewModel.urlLiveData.observe(this, Observer { + viewModel.urlLiveData.observe(this, { upJavaScriptEnable() web_view.loadUrl(it.url, it.headerMap) }) diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt index f5fa3f97b..11b4ba940 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -17,7 +17,7 @@ import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssStar import io.legado.app.help.http.HttpHelper -import io.legado.app.model.Rss +import io.legado.app.model.rss.Rss import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.DocumentUtils import io.legado.app.utils.FileUtils diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt index b5a92d099..48180951f 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt @@ -10,19 +10,19 @@ import android.view.View import android.view.ViewGroup import android.widget.EditText import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.Observer 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.Theme +import io.legado.app.constant.AppPattern import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* @@ -51,9 +51,10 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + tool_bar.setBackgroundColor(primaryColor) tool_bar.title = getString(R.string.group_manage) tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) @@ -62,10 +63,10 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener tv_ok.setTextColor(requireContext().accentColor) tv_ok.visible() tv_ok.onClick { dismiss() } - App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { + App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, { val groups = linkedSetOf() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } adapter.setItems(groups.toList()) }) diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt index d28ae94a8..64fc6a849 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt @@ -11,13 +11,13 @@ import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern import io.legado.app.data.entities.RssSource import io.legado.app.help.IntentDataHelp import io.legado.app.help.ItemTouchCallback @@ -142,10 +142,10 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r } private fun initLiveDataGroup() { - App.db.rssSourceDao().liveGroup().observe(this, Observer { + App.db.rssSourceDao().liveGroup().observe(this, { groups.clear() it.map { group -> - groups.addAll(group.splitNotBlank(",", ";")) + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupMenu() }) @@ -195,7 +195,7 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r } else { App.db.rssSourceDao().liveSearch("%$key%") } - sourceLiveData?.observe(this, Observer { + sourceLiveData?.observe(this, { val diffResult = DiffUtil .calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItems(it, diffResult) diff --git a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt index d9725f019..3141c5a86 100644 --- a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt @@ -5,6 +5,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect +import android.graphics.Typeface import android.util.AttributeSet import androidx.annotation.ColorInt import androidx.appcompat.widget.AppCompatTextView @@ -21,6 +22,7 @@ class BatteryView(context: Context, attrs: AttributeSet?) : AppCompatTextView(co batteryPaint.strokeWidth = 1.dp.toFloat() batteryPaint.isAntiAlias = true batteryPaint.color = paint.color + typeface = Typeface.createFromAsset(context.assets, "number.ttf") } fun setColor(@ColorInt color: Int) { diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index 94d3b7be6..3b9d28757 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -45,8 +45,6 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a private val attachToActivity: Boolean init { - inflate(context, R.layout.view_title_bar, this) - toolbar = findViewById(R.id.toolbar) val a = context.obtainStyledAttributes( attrs, R.styleable.TitleBar, R.attr.titleBarStyle, 0 @@ -62,6 +60,12 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a val titleText = a.getString(R.styleable.TitleBar_title) val subtitleText = a.getString(R.styleable.TitleBar_subtitle) + when (a.getInt(R.styleable.TitleBar_themeMode, 0)) { + 1 -> inflate(context, R.layout.view_title_bar_dark, this) + else -> inflate(context, R.layout.view_title_bar, this) + } + toolbar = findViewById(R.id.toolbar) + toolbar.apply { navigationIcon?.let { this.navigationIcon = it diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt index 57611e8ad..697c3f68c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt @@ -1,6 +1,5 @@ package io.legado.app.ui.widget.font -import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri @@ -22,7 +21,7 @@ import io.legado.app.help.AppConfig import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FilePicker import io.legado.app.utils.* @@ -38,6 +37,7 @@ class FontSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener, FontAdapter.CallBack { private val fontFolderRequestCode = 35485 + private val fontRegex = Regex(".*\\.[ot]tf") private val fontFolder by lazy { FileUtils.createFolderIfNotExist(App.INSTANCE.filesDir, "Fonts") } @@ -59,9 +59,10 @@ class FontSelectDialog : BaseDialogFragment(), } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(requireContext().bottomBackground) + tool_bar.setBackgroundColor(primaryColor) tool_bar.setTitle(R.string.select_font) tool_bar.inflateMenu(R.menu.font_select) + tool_bar.menu.applyTint(requireContext()) tool_bar.setOnMenuItemClickListener(this) adapter = FontAdapter(requireContext(), this) recycler_view.layoutManager = LinearLayoutManager(context) @@ -74,12 +75,12 @@ class FontSelectDialog : BaseDialogFragment(), if (fontPath.isContentPath()) { val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath)) if (doc?.canRead() == true) { - getFontFiles(doc) + loadFontFiles(doc) } else { openFolder() } } else { - getFontFilesByPermission(fontPath) + loadFontFilesByPermission(fontPath) } } } @@ -110,22 +111,21 @@ class FontSelectDialog : BaseDialogFragment(), FilePicker.selectFolder(this@FontSelectDialog, fontFolderRequestCode) { val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts" putPrefString(PreferKey.fontFolder, path) - getFontFilesByPermission(path) + loadFontFilesByPermission(path) } } } - @SuppressLint("DefaultLocale") - private fun getFontFiles(doc: DocumentFile) { + private fun loadFontFiles(doc: DocumentFile) { execute { val fontItems = arrayListOf() val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri) docItems.forEach { item -> - if (item.name.toLowerCase().matches(".*\\.[ot]tf".toRegex())) { + if (item.name.toLowerCase(Locale.getDefault()).matches(fontRegex)) { fontItems.add(item) } } - fontItems + fontItems.sortedBy { it.name } }.onSuccess { adapter?.setItems(it) }.onError { @@ -133,36 +133,41 @@ class FontSelectDialog : BaseDialogFragment(), } } - @SuppressLint("DefaultLocale") - private fun getFontFilesByPermission(path: String) { + private fun loadFontFilesByPermission(path: String) { PermissionsCompat.Builder(this@FontSelectDialog) .addPermissions(*Permissions.Group.STORAGE) .rationale(R.string.tip_perm_request_storage) .onGranted { - try { - val fontItems = arrayListOf() - val file = File(path) - file.listFiles { pathName -> - pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) - }?.forEach { - fontItems.add( - DocItem( - it.name, - it.extension, - it.length(), - Date(it.lastModified()), - Uri.parse(it.absolutePath) - ) - ) - } - adapter?.setItems(fontItems) - } catch (e: Exception) { - toast(e.localizedMessage ?: "") - } + loadFontFiles(path) } .request() } + private fun loadFontFiles(path: String) { + execute { + val fontItems = arrayListOf() + val file = File(path) + file.listFiles { pathName -> + pathName.name.toLowerCase(Locale.getDefault()).matches(fontRegex) + }?.forEach { + fontItems.add( + DocItem( + it.name, + it.extension, + it.length(), + Date(it.lastModified()), + Uri.parse(it.absolutePath) + ) + ) + } + fontItems.sortedBy { it.name } + }.onSuccess { + adapter?.setItems(it) + }.onError { + toast("getFontFiles:${it.localizedMessage}") + } + } + override fun onClick(docItem: DocItem) { execute { fontFolder.listFiles()?.forEach { @@ -197,7 +202,7 @@ class FontSelectDialog : BaseDialogFragment(), when (requestCode) { fontFolderRequestCode -> { putPrefString(PreferKey.fontFolder, currentPath) - getFontFilesByPermission(currentPath) + loadFontFilesByPermission(currentPath) } } } @@ -214,10 +219,10 @@ class FontSelectDialog : BaseDialogFragment(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) - getFontFiles(doc) + loadFontFiles(doc) } else { RealPathUtil.getPath(requireContext(), uri)?.let { - getFontFilesByPermission(it) + loadFontFilesByPermission(it) } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt index e8d6ebf61..4bc309b56 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt @@ -4,12 +4,10 @@ import android.view.MotionEvent import kotlin.math.abs import kotlin.math.atan -class RotateGestureDetector(l: OnRotateListener) { +class RotateGestureDetector(private val mListener: OnRotateListener) { private val MAX_DEGREES_STEP = 120 - private val mListener: OnRotateListener? = null - private var mPrevSlope = 0f private var mCurrSlope = 0f diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt index 7ce8c9dfb..042b03868 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt @@ -66,7 +66,7 @@ class Preference(context: Context, attrs: AttributeSet) : } val iconView = it.findViewById(R.id.preference_icon) if (iconView is ImageView) { - iconView.isVisible = icon != null && icon.isVisible + iconView.isVisible = icon != null iconView.setImageDrawable(icon) iconView.setColorFilter(context.accentColor) } diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.java b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.java new file mode 100644 index 000000000..a8291897e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.java @@ -0,0 +1,983 @@ +/* + * Copyright 2020 Mupceet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.legado.app.ui.widget.recycler; + +import android.content.res.Resources; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import io.legado.app.BuildConfig; + +/** + * @author mupceet + */ +public class DragSelectTouchHelper { + private static final String TAG = "DSTH"; + private static final float MAX_HOTSPOT_RATIO = 0.5f; + private static final EdgeType DEFAULT_EDGE_TYPE = EdgeType.INSIDE_EXTEND; + private static final float DEFAULT_HOTSPOT_RATIO = 0.2f; + private static final int DEFAULT_HOTSPOT_OFFSET = 0; + private static final int DEFAULT_MAX_SCROLL_VELOCITY = 10; + private static final int SELECT_STATE_NORMAL = 0x00; + + /* + * !autoChangeMode +-------------------+ inactiveSelect() + * +------------------------------------> | | <--------------------+ + * | | Normal | | + * | activeDragSelect(position) | | activeSlideSelect() | + * | +------------------------------ | | ----------+ | + * | v +-------------------+ v | + * +-------------------+ autoChangeMode +-----------------------+ + * | Drag From Disable | ----------------------------------------------> | | + * +-------------------+ | | + * | | | | + * | | activeDragSelect(position) && allowDragInSlide | Slide | + * | | <---------------------------------------------- | | + * | Drag From Slide | | | + * | | | | + * | | ----------------------------------------------> | | + * +-------------------+ +-----------------------+ + */ + + private static final int SELECT_STATE_SLIDE = 0x01; + private static final int SELECT_STATE_DRAG_FROM_NORMAL = 0x10; + private static final int SELECT_STATE_DRAG_FROM_SLIDE = 0x11; + private final DisplayMetrics mDisplayMetrics; + /** + * Start of the slide area. + */ + private float mSlideAreaLeft; + /** + * End of the slide area. + */ + private float mSlideAreaRight; + /** + * The hotspot height by the ratio of RecyclerView. + */ + private float mHotspotHeightRatio; + /** + * The hotspot height. + */ + private float mHotspotHeight = 0f; + /** + * The hotspot offset. + */ + private float mHotspotOffset; + /** + * Whether should continue scrolling when move outside top hotspot region. + */ + private boolean mScrollAboveTopRegion; + /** + * Whether should continue scrolling when move outside bottom hotspot region. + */ + private boolean mScrollBelowBottomRegion; + /** + * The maximum velocity of auto scrolling. + */ + private int mMaximumVelocity; + /** + * Whether should auto enter slide mode after drag select finished. + */ + private boolean mShouldAutoChangeState; + /** + * Whether can drag selection in slide select mode. + */ + private boolean mIsAllowDragInSlideState; + private RecyclerView mRecyclerView = null; + /** + * The coordinate of hotspot area. + */ + private float mTopRegionFrom = -1f; + private float mTopRegionTo = -1f; + private float mBottomRegionFrom = -1f; + private float mBottomRegionTo = -1f; + private final View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (oldLeft != left || oldRight != right || oldTop != top || oldBottom != bottom) { + if (v == mRecyclerView) { + Logger.i("onLayoutChange:new: " + + left + " " + top + " " + right + " " + bottom); + Logger.i("onLayoutChange:old: " + + oldLeft + " " + oldTop + " " + oldRight + " " + oldBottom); + init(bottom - top); + } + } + } + }; + /** + * The current mode of selection. + */ + private int mSelectState = SELECT_STATE_NORMAL; + /** + * Whether is in top hotspot area. + */ + private boolean mIsInTopHotspot = false; + /** + * Whether is in bottom hotspot area. + */ + private boolean mIsInBottomHotspot = false; + /** + * Indicates automatically scroll. + */ + private boolean mIsScrolling; + /** + * The actual speed of the current moment. + */ + private int mScrollDistance = 0; + /** + * The reference coordinate for the action start, used to avoid reverse scrolling. + */ + private float mDownY = Float.MIN_VALUE; + /** + * The reference coordinates for the last action. + */ + private float mLastX = Float.MIN_VALUE; + private float mLastY = Float.MIN_VALUE; + /** + * The selected items position. + */ + private int mStart = RecyclerView.NO_POSITION; + private int mEnd = RecyclerView.NO_POSITION; + private int mLastRealStart = RecyclerView.NO_POSITION; + private int mLastRealEnd = RecyclerView.NO_POSITION; + private int mSlideStateStartPosition = RecyclerView.NO_POSITION; + private boolean mHaveCalledSelectStart = false; + /** + * Developer callback which controls the behavior of DragSelectTouchHelper. + */ + @NonNull + private Callback mCallback; + private Runnable mScrollRunnable = new Runnable() { + @Override + public void run() { + if (mIsScrolling) { + scrollBy(mScrollDistance); + ViewCompat.postOnAnimation(mRecyclerView, mScrollRunnable); + } + } + }; + private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + Logger.d("onInterceptTouchEvent: x:" + e.getX() + ",y:" + e.getY() + + ", " + MotionEvent.actionToString(e.getAction())); + RecyclerView.Adapter adapter = rv.getAdapter(); + if (adapter == null || adapter.getItemCount() == 0) { + return false; + } + boolean intercept = false; + int action = e.getAction(); + int actionMask = action & MotionEvent.ACTION_MASK; + // It seems that it's unnecessary to process multiple pointers. + switch (actionMask) { + case MotionEvent.ACTION_DOWN: + mDownY = e.getY(); + // call the selection start's callback before moving + if (mSelectState == SELECT_STATE_SLIDE && isInSlideArea(e)) { + mSlideStateStartPosition = getItemPosition(rv, e); + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + mCallback.onSelectStart(mSlideStateStartPosition); + mHaveCalledSelectStart = true; + } + intercept = true; + } + break; + case MotionEvent.ACTION_MOVE: + if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL + || mSelectState == SELECT_STATE_DRAG_FROM_SLIDE) { + Logger.i("onInterceptTouchEvent: drag mode move"); + intercept = true; + } + break; + case MotionEvent.ACTION_UP: + if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL + || mSelectState == SELECT_STATE_DRAG_FROM_SLIDE) { + intercept = true; + } + // fall through + case MotionEvent.ACTION_CANCEL: + // finger is lifted before moving + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFinished(mSlideStateStartPosition); + mSlideStateStartPosition = RecyclerView.NO_POSITION; + } + // selection has triggered + if (mStart != RecyclerView.NO_POSITION) { + selectFinished(mEnd); + } + break; + default: + // do nothing + } + // Intercept only when the selection is triggered + Logger.d("intercept result: " + intercept); + return intercept; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + if (!isActivated()) { + return; + } + Logger.d("onTouchEvent: x:" + e.getX() + ",y:" + e.getY() + + ", " + MotionEvent.actionToString(e.getAction())); + int action = e.getAction(); + int actionMask = action & MotionEvent.ACTION_MASK; + switch (actionMask) { + case MotionEvent.ACTION_MOVE: + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFirstItem(mSlideStateStartPosition); + // selection is triggered + mSlideStateStartPosition = RecyclerView.NO_POSITION; + Logger.i("onTouchEvent: after slide mode down"); + } + processAutoScroll(e); + if (!mIsInTopHotspot && !mIsInBottomHotspot) { + updateSelectedRange(rv, e); + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + selectFinished(mEnd); + break; + default: + // do nothing + } + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + inactiveSelect(); + } + } + }; + + public DragSelectTouchHelper(@NonNull Callback callback) { + mCallback = callback; + mDisplayMetrics = Resources.getSystem().getDisplayMetrics(); + setHotspotRatio(DEFAULT_HOTSPOT_RATIO); + setHotspotOffset(DEFAULT_HOTSPOT_OFFSET); + setMaximumVelocity(DEFAULT_MAX_SCROLL_VELOCITY); + setEdgeType(DEFAULT_EDGE_TYPE); + setAutoEnterSlideState(false); + setAllowDragInSlideState(false); + setSlideArea(0, 0); + } + + /** + * Attaches the DragSelectTouchHelper to the provided RecyclerView. If TouchHelper is already + * attached to a RecyclerView, it will first detach from the previous one. You can call this + * method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove DragSelectTouchHelper from the + * current RecyclerView. + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); + mRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener); + } + } + + /** + * Activate the slide selection mode. + */ + public void activeSlideSelect() { + activeSelectInternal(RecyclerView.NO_POSITION); + } + + /** + * Activate the selection mode with selected item position. Normally called on long press. + * + * @param position Indicates the position of selected item. + */ + public void activeDragSelect(int position) { + activeSelectInternal(position); + } + + /** + * Exit the selection mode. + */ + public void inactiveSelect() { + if (isActivated()) { + selectFinished(mEnd); + } else { + selectFinished(RecyclerView.NO_POSITION); + } + Logger.logSelectStateChange(mSelectState, SELECT_STATE_NORMAL); + mSelectState = SELECT_STATE_NORMAL; + } + + /** + * To determine whether it is in the selection mode. + * + * @return true if is in the selection mode. + */ + public boolean isActivated() { + return (mSelectState != SELECT_STATE_NORMAL); + } + + /** + * Sets hotspot height by ratio of RecyclerView. + * + * @param ratio range (0, 0.5). + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setHotspotRatio(float ratio) { + mHotspotHeightRatio = ratio; + return this; + } + + /** + * Sets hotspot height. + * + * @param hotspotHeight hotspot height which unit is dp. + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setHotspotHeight(int hotspotHeight) { + mHotspotHeight = dp2px(hotspotHeight); + return this; + } + + /** + * Sets hotspot offset. It don't need to be set if no special requirement. + * + * @param hotspotOffset hotspot offset which unit is dp. + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setHotspotOffset(int hotspotOffset) { + mHotspotOffset = dp2px(hotspotOffset); + return this; + } + + /** + * Sets the activation edge type, one of: + *
    + *
  • {@link EdgeType#INSIDE} for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + *
  • {@link EdgeType#INSIDE_EXTEND} for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + *
+ * + * @param type The type of edge to use. + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setEdgeType(EdgeType type) { + switch (type) { + case INSIDE: + mScrollAboveTopRegion = false; + mScrollBelowBottomRegion = false; + break; + case INSIDE_EXTEND: + default: + mScrollAboveTopRegion = true; + mScrollBelowBottomRegion = true; + break; + } + return this; + } + + /** + * Sets sliding area's start and end, has been considered RTL situation + * + * @param startDp The start of the sliding area + * @param endDp The end of the sliding area + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setSlideArea(int startDp, int endDp) { + if (!isRtl()) { + mSlideAreaLeft = dp2px(startDp); + mSlideAreaRight = dp2px(endDp); + } else { + int displayWidth = mDisplayMetrics.widthPixels; + mSlideAreaLeft = displayWidth - dp2px(endDp); + mSlideAreaRight = displayWidth - dp2px(startDp); + } + return this; + } + + /** + * Sets the maximum velocity for scrolling + * + * @param velocity maximum velocity + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setMaximumVelocity(int velocity) { + mMaximumVelocity = (int) (velocity * mDisplayMetrics.density + 0.5f); + return this; + } + + /** + * Sets whether should auto enter slide mode after drag select finished. + * It's usefully for LinearLayout RecyclerView. + * + * @param autoEnterSlideState should auto enter slide mode + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setAutoEnterSlideState(boolean autoEnterSlideState) { + mShouldAutoChangeState = autoEnterSlideState; + return this; + } + + /** + * Sets whether can drag selection in slide select mode. + * It's usefully for LinearLayout RecyclerView. + * + * @param allowDragInSlideState allow drag selection in slide select mode + * @return The select helper, which may used to chain setter calls. + */ + public DragSelectTouchHelper setAllowDragInSlideState(boolean allowDragInSlideState) { + mIsAllowDragInSlideState = allowDragInSlideState; + return this; + } + + private void init(int rvHeight) { + if (mHotspotOffset >= rvHeight * MAX_HOTSPOT_RATIO) { + mHotspotOffset = rvHeight * MAX_HOTSPOT_RATIO; + } + // The height of hotspot area is not set, using (RV height x ratio) + if (mHotspotHeight <= 0) { + if (mHotspotHeightRatio <= 0 || mHotspotHeightRatio >= MAX_HOTSPOT_RATIO) { + mHotspotHeightRatio = DEFAULT_HOTSPOT_RATIO; + } + mHotspotHeight = rvHeight * mHotspotHeightRatio; + } else { + if (mHotspotHeight >= rvHeight * MAX_HOTSPOT_RATIO) { + mHotspotHeight = rvHeight * MAX_HOTSPOT_RATIO; + } + } + + mTopRegionFrom = mHotspotOffset; + mTopRegionTo = mTopRegionFrom + mHotspotHeight; + mBottomRegionTo = rvHeight - mHotspotOffset; + mBottomRegionFrom = mBottomRegionTo - mHotspotHeight; + + if (mTopRegionTo > mBottomRegionFrom) { + mTopRegionTo = mBottomRegionFrom = rvHeight >> 1; + } + + Logger.d("Hotspot: [" + mTopRegionFrom + ", " + mTopRegionTo + "], [" + + mBottomRegionFrom + ", " + mBottomRegionTo + "]"); + } + + private void activeSelectInternal(int position) { + // We should initialize the hotspot here, because its data may be delayed load + if (mRecyclerView != null) { + init(mRecyclerView.getHeight()); + } + if (position == RecyclerView.NO_POSITION) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); + mSelectState = SELECT_STATE_SLIDE; + } else { + if (!mHaveCalledSelectStart) { + mCallback.onSelectStart(position); + mHaveCalledSelectStart = true; + } + if (mSelectState == SELECT_STATE_SLIDE) { + if (mIsAllowDragInSlideState && selectFirstItem(position)) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_SLIDE); + mSelectState = SELECT_STATE_DRAG_FROM_SLIDE; + } + } else if (mSelectState == SELECT_STATE_NORMAL) { + if (selectFirstItem(position)) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_NORMAL); + mSelectState = SELECT_STATE_DRAG_FROM_NORMAL; + } + } else { + Logger.e("activeSelect in unexpected state: " + mSelectState); + } + } + } + + private boolean selectFirstItem(int position) { + boolean selectFirstItemSucceed = mCallback.onSelectChange(position, true); + // The drag select feature is only available if the first item is available for selection + if (selectFirstItemSucceed) { + mStart = position; + mEnd = position; + mLastRealStart = position; + mLastRealEnd = position; + } + return selectFirstItemSucceed; + } + + private void updateSelectedRange(RecyclerView rv, MotionEvent e) { + updateSelectedRange(rv, e.getX(), e.getY()); + } + + private void updateSelectedRange(RecyclerView rv, float x, float y) { + int position = getItemPosition(rv, x, y); + if (position != RecyclerView.NO_POSITION && mEnd != position) { + mEnd = position; + notifySelectRangeChange(); + } + } + + private void notifySelectRangeChange() { + if (mStart == RecyclerView.NO_POSITION || mEnd == RecyclerView.NO_POSITION) { + return; + } + + int newStart, newEnd; + newStart = Math.min(mStart, mEnd); + newEnd = Math.max(mStart, mEnd); + if (mLastRealStart == RecyclerView.NO_POSITION || mLastRealEnd == RecyclerView.NO_POSITION) { + if (newEnd - newStart == 1) { + notifySelectChange(newStart, newStart, true); + } else { + notifySelectChange(newStart, newEnd, true); + } + } else { + if (newStart > mLastRealStart) { + notifySelectChange(mLastRealStart, newStart - 1, false); + } else if (newStart < mLastRealStart) { + notifySelectChange(newStart, mLastRealStart - 1, true); + } + + if (newEnd > mLastRealEnd) { + notifySelectChange(mLastRealEnd + 1, newEnd, true); + } else if (newEnd < mLastRealEnd) { + notifySelectChange(newEnd + 1, mLastRealEnd, false); + } + } + + mLastRealStart = newStart; + mLastRealEnd = newEnd; + } + + private void notifySelectChange(int start, int end, boolean newState) { + for (int i = start; i <= end; i++) { + mCallback.onSelectChange(i, newState); + } + } + + private void selectFinished(int lastItem) { + if (lastItem != RecyclerView.NO_POSITION) { + mCallback.onSelectEnd(lastItem); + } + mStart = RecyclerView.NO_POSITION; + mEnd = RecyclerView.NO_POSITION; + mLastRealStart = RecyclerView.NO_POSITION; + mLastRealEnd = RecyclerView.NO_POSITION; + mHaveCalledSelectStart = false; + mIsInTopHotspot = false; + mIsInBottomHotspot = false; + stopAutoScroll(); + switch (mSelectState) { + case SELECT_STATE_DRAG_FROM_NORMAL: + if (mShouldAutoChangeState) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); + mSelectState = SELECT_STATE_SLIDE; + } else { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_NORMAL); + mSelectState = SELECT_STATE_NORMAL; + } + break; + case SELECT_STATE_DRAG_FROM_SLIDE: + Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); + mSelectState = SELECT_STATE_SLIDE; + break; + default: + // doesn't change the selection state + break; + } + } + + /** + * Process motion event, according to the location to determine whether to scroll + */ + private void processAutoScroll(MotionEvent e) { + float y = e.getY(); + if (y >= mTopRegionFrom && y <= mTopRegionTo && y < mDownY) { + mLastX = e.getX(); + mLastY = e.getY(); + float scrollDistanceFactor = (y - mTopRegionTo) / mHotspotHeight; + mScrollDistance = (int) (mMaximumVelocity * scrollDistanceFactor); + if (!mIsInTopHotspot) { + mIsInTopHotspot = true; + startAutoScroll(); + mDownY = mTopRegionTo; + } + } else if (mScrollAboveTopRegion && y < mTopRegionFrom && mIsInTopHotspot) { + mLastX = e.getX(); + mLastY = mTopRegionFrom; + // Use the maximum speed + mScrollDistance = mMaximumVelocity * -1; + startAutoScroll(); + } else if (y >= mBottomRegionFrom && y <= mBottomRegionTo && y > mDownY) { + mLastX = e.getX(); + mLastY = e.getY(); + float scrollDistanceFactor = (y - mBottomRegionFrom) / mHotspotHeight; + mScrollDistance = (int) (mMaximumVelocity * scrollDistanceFactor); + if (!mIsInBottomHotspot) { + mIsInBottomHotspot = true; + startAutoScroll(); + mDownY = mBottomRegionFrom; + } + } else if (mScrollBelowBottomRegion && y > mBottomRegionTo && mIsInBottomHotspot) { + mLastX = e.getX(); + mLastY = mBottomRegionTo; + // Use the maximum speed + mScrollDistance = mMaximumVelocity; + startAutoScroll(); + } else { + mIsInTopHotspot = false; + mIsInBottomHotspot = false; + mLastX = Float.MIN_VALUE; + mLastY = Float.MIN_VALUE; + stopAutoScroll(); + } + + } + + private void startAutoScroll() { + if (!mIsScrolling) { + mIsScrolling = true; + mRecyclerView.removeCallbacks(mScrollRunnable); + ViewCompat.postOnAnimation(mRecyclerView, mScrollRunnable); + } + } + + private void stopAutoScroll() { + if (mIsScrolling) { + mIsScrolling = false; + mRecyclerView.removeCallbacks(mScrollRunnable); + } + } + + private void scrollBy(int distance) { + int scrollDistance; + if (distance > 0) { + scrollDistance = Math.min(distance, mMaximumVelocity); + } else { + scrollDistance = Math.max(distance, -mMaximumVelocity); + } + mRecyclerView.scrollBy(0, scrollDistance); + if (mLastX != Float.MIN_VALUE && mLastY != Float.MIN_VALUE) { + updateSelectedRange(mRecyclerView, mLastX, mLastY); + } + } + + private int dp2px(float dpVal) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + dpVal, mDisplayMetrics); + } + + private boolean isRtl() { + return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL; + } + + private boolean isInSlideArea(MotionEvent e) { + float x = e.getX(); + return (x > mSlideAreaLeft && x < mSlideAreaRight); + } + + private int getItemPosition(RecyclerView rv, MotionEvent e) { + return getItemPosition(rv, e.getX(), e.getY()); + } + + private int getItemPosition(RecyclerView rv, float x, float y) { + final View v = rv.findChildViewUnder(x, y); + if (v == null) { + RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + int lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + int lastItemPosition = layoutManager.getItemCount() - 1; + if (lastItemPosition == lastVisibleItemPosition) { + return lastItemPosition; + } + } + return RecyclerView.NO_POSITION; + } + return rv.getChildAdapterPosition(v); + } + + /** + * Edge type that specifies an activation area starting at the view bounds and extending inward. + */ + public enum EdgeType { + /** + * After activation begins, moving outside the view bounds will stop scrolling. + */ + INSIDE, + + /** + * After activation begins, moving outside the view bounds will continue scrolling. + */ + INSIDE_EXTEND + } + + /** + * This class is the contract between DragSelectTouchHelper and your application. It lets you + * update adapter when selection start/end and state changed. + */ + public abstract static class Callback { + /** + * Called when changing item state. + * + * @param position this item want to change the state to new state. + * @param isSelected true if the position should be selected, false otherwise. + * @return Whether to set the new state successfully. + */ + public abstract boolean onSelectChange(int position, boolean isSelected); + + /** + * Called when selection start. + * + * @param start the first selected item. + */ + public void onSelectStart(int start) { + + } + + /** + * Called when selection end. + * + * @param end the last selected item. + */ + public void onSelectEnd(int end) { + + } + } + + /** + * An advance Callback which provide 4 useful selection modes {@link Mode}. + *

+ * Note: Since the state of item may be repeatedly set, in order to improve efficiency, + * please process it in the Adapter + */ + public abstract static class AdvanceCallback extends Callback { + private Mode mMode; + private Set mOriginalSelection; + private boolean mFirstWasSelected; + + /** + * Creates a SimpleCallback with default {@link Mode#SelectAndReverse}# mode. + * + * @see Mode + */ + public AdvanceCallback() { + setMode(Mode.SelectAndReverse); + } + + /** + * Creates a SimpleCallback with select mode. + * + * @param mode the initial select mode + * @see Mode + */ + public AdvanceCallback(Mode mode) { + setMode(mode); + } + + /** + * Sets the select mode. + * + * @param mode The type of select mode. + * @see Mode + */ + public void setMode(Mode mode) { + mMode = mode; + } + + @Override + public void onSelectStart(int start) { + mOriginalSelection = new HashSet<>(); + Set selected = currentSelectedId(); + if (selected != null) { + mOriginalSelection.addAll(selected); + } + mFirstWasSelected = mOriginalSelection.contains(getItemId(start)); + } + + @Override + public void onSelectEnd(int end) { + mOriginalSelection = null; + } + + @Override + public boolean onSelectChange(int position, boolean isSelected) { + boolean stateChanged; + switch (mMode) { + case SelectAndKeep: { + stateChanged = updateSelectState(position, true); + break; + } + case SelectAndReverse: { + stateChanged = updateSelectState(position, isSelected); + break; + } + case SelectAndUndo: { + if (isSelected) { + stateChanged = updateSelectState(position, true); + } else { + stateChanged = updateSelectState(position, mOriginalSelection.contains(getItemId(position))); + } + break; + } + case ToggleAndKeep: { + stateChanged = updateSelectState(position, !mFirstWasSelected); + break; + } + case ToggleAndReverse: { + if (isSelected) { + stateChanged = updateSelectState(position, !mFirstWasSelected); + } else { + stateChanged = updateSelectState(position, mFirstWasSelected); + } + break; + } + case ToggleAndUndo: { + if (isSelected) { + stateChanged = updateSelectState(position, !mFirstWasSelected); + } else { + stateChanged = updateSelectState(position, mOriginalSelection.contains(getItemId(position))); + } + break; + } + default: + // SelectAndReverse Mode + stateChanged = updateSelectState(position, isSelected); + } + return stateChanged; + } + + /** + * Get the currently selected items when selecting first item. + * + * @return the currently selected item's id set. + */ + public abstract Set currentSelectedId(); + + /** + * Get the ID of the item. + * + * @param position item position to be judged. + * @return item's identity. + */ + public abstract T getItemId(int position); + + /** + * Update the selection status of the position. + * + * @param position the position who's selection state changed. + * @param isSelected true if the position should be selected, false otherwise. + * @return Whether to set the state successfully. + */ + public abstract boolean updateSelectState(int position, boolean isSelected); + + /** + * Different existing selection modes + */ + public enum Mode { + /** + * Selects the first item and applies the same state to each item you go by + * and keep the state on move back + */ + SelectAndKeep, + /** + * Selects the first item and applies the same state to each item you go by + * and applies inverted state on move back + */ + SelectAndReverse, + /** + * Selects the first item and applies the same state to each item you go by + * and reverts to the original state on move back + */ + SelectAndUndo, + /** + * Toggles the first item and applies the same state to each item you go by + * and keep the state on move back + */ + ToggleAndKeep, + /** + * Toggles the first item and applies the same state to each item you go by + * and applies inverted state on move back + */ + ToggleAndReverse, + /** + * Toggles the first item and applies the same state to each item you go by + * and reverts to the original state on move back + */ + ToggleAndUndo, + } + } + + private static class Logger { + private static boolean DEBUG = BuildConfig.DEBUG; + + private static void d(String msg) { + if (DEBUG) { + Log.d(TAG, msg); + } + } + + private static void e(String msg) { + Log.e(TAG, msg); + } + + private static void i(String msg) { + Log.i(TAG, msg); + } + + private static void logSelectStateChange(int before, int after) { + i("Select state changed: " + stateName(before) + " --> " + stateName(after)); + } + + private static String stateName(int state) { + switch (state) { + case SELECT_STATE_NORMAL: + return "NormalState"; + case SELECT_STATE_SLIDE: + return "SlideState"; + case SELECT_STATE_DRAG_FROM_NORMAL: + return "DragFromNormal"; + case SELECT_STATE_DRAG_FROM_SLIDE: + return "DragFromSlide"; + default: + return "Unknown"; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index df9f457cd..1734997f2 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -24,6 +24,7 @@ import io.legado.app.App import io.legado.app.BuildConfig import io.legado.app.R import org.jetbrains.anko.defaultSharedPreferences +import org.jetbrains.anko.longToast import org.jetbrains.anko.toast import java.io.File import java.io.FileOutputStream @@ -138,7 +139,7 @@ fun Context.sendToClip(text: String) { val clipData = ClipData.newPlainText(null, text) clipboard?.let { clipboard.setPrimaryClip(clipData) - toast(R.string.copy_complete) + longToast(R.string.copy_complete) } } diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt index b706a8d5e..4a3e236cb 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -98,6 +98,14 @@ object NetworkUtils { } else url.substring(0, index) } + fun getSubDomain(url: String?): String { + var baseUrl = getBaseUrl(url) + if (baseUrl == null) return "" + return if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) { + baseUrl.substring(baseUrl.lastIndexOf("/")+1) + } else baseUrl.substring(baseUrl.indexOf(".")+1) + } + /** * Get local Ip address. */ diff --git a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt b/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt index 9871ff72a..dba45d4c1 100644 --- a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt +++ b/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt @@ -4,7 +4,7 @@ import io.legado.app.App import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.help.BookHelp -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.ReadBook import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject @@ -94,6 +94,7 @@ object BookshelfController { App.db.bookDao().update(book) if (ReadBook.book?.bookUrl == book.bookUrl) { ReadBook.book = book + ReadBook.durChapterIndex = index } } } diff --git a/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt index 77b4384c1..ae3172d2f 100644 --- a/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt +++ b/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt @@ -6,7 +6,7 @@ import fi.iki.elonen.NanoWSD import io.legado.app.App import io.legado.app.R import io.legado.app.model.Debug -import io.legado.app.model.WebBook +import io.legado.app.model.webBook.WebBook import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.utils.isJson diff --git a/app/src/main/res/drawable/ic_outline_cloud_24.xml b/app/src/main/res/drawable/ic_outline_cloud_24.xml new file mode 100644 index 000000000..c8b092ead --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_cloud_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-land/activity_book_info.xml b/app/src/main/res/layout-land/activity_book_info.xml index d99efc484..05a4b316a 100644 --- a/app/src/main/res/layout-land/activity_book_info.xml +++ b/app/src/main/res/layout-land/activity_book_info.xml @@ -19,7 +19,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" - app:title="@string/book_info" /> + app:title="@string/book_info" + app:themeMode="dark" /> + app:layout_constraintTop_toTopOf="parent" + app:themeMode="dark" /> + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/title_bar" /> + app:tint="@color/md_white_1000" /> + app:tint="@color/md_white_1000" /> + app:tint="@color/md_white_1000" /> + app:tint="@color/md_white_1000" /> + app:tint="@color/md_white_1000" /> + android:contentDescription="@string/chapter_list" + app:tint="@color/md_white_1000" /> diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml index 02aaf18f3..e2d35458f 100644 --- a/app/src/main/res/layout/activity_book_info.xml +++ b/app/src/main/res/layout/activity_book_info.xml @@ -26,7 +26,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" - app:title="@string/book_info" /> + app:title="@string/book_info" + app:themeMode="dark" /> + android:layout_height="wrap_content" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> diff --git a/app/src/main/res/layout/dialog_change_source.xml b/app/src/main/res/layout/dialog_change_source.xml index 4b8703b37..d72fab249 100644 --- a/app/src/main/res/layout/dialog_change_source.xml +++ b/app/src/main/res/layout/dialog_change_source.xml @@ -12,6 +12,9 @@ android:layout_height="wrap_content" android:background="@color/background_menu" android:elevation="5dp" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" app:displayHomeAsUp="false" app:fitStatusBar="false" /> diff --git a/app/src/main/res/layout/dialog_file_chooser.xml b/app/src/main/res/layout/dialog_file_chooser.xml index 7615ba752..efe1db737 100644 --- a/app/src/main/res/layout/dialog_file_chooser.xml +++ b/app/src/main/res/layout/dialog_file_chooser.xml @@ -1,5 +1,6 @@ + android:layout_height="wrap_content" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> + android:layout_height="wrap_content" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_recycler_view.xml b/app/src/main/res/layout/dialog_recycler_view.xml index c8e7c0944..e7ca72d9a 100644 --- a/app/src/main/res/layout/dialog_recycler_view.xml +++ b/app/src/main/res/layout/dialog_recycler_view.xml @@ -1,6 +1,7 @@ @@ -8,7 +9,10 @@ + android:layout_height="wrap_content" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> + + + android:layout_height="wrap_content" + android:theme="?attr/actionBarStyle" + app:titleTextAppearance="@style/ToolbarTitle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> - + android:singleLine="true" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/iv_checked" /> + android:singleLine="true" + app:layout_constraintTop_toBottomOf="@+id/tv_chapter_name" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/iv_checked" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_http_tts.xml b/app/src/main/res/layout/item_http_tts.xml new file mode 100644 index 000000000..fae2a3bcc --- /dev/null +++ b/app/src/main/res/layout/item_http_tts.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_read_record.xml b/app/src/main/res/layout/item_read_record.xml index b28197be6..41666c1af 100644 --- a/app/src/main/res/layout/item_read_record.xml +++ b/app/src/main/res/layout/item_read_record.xml @@ -1,25 +1,37 @@ + android:orientation="horizontal" + android:gravity="center_vertical"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_title_bar_dark.xml b/app/src/main/res/layout/view_title_bar_dark.xml new file mode 100644 index 000000000..72e7fa3b3 --- /dev/null +++ b/app/src/main/res/layout/view_title_bar_dark.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/menu/speak_engine.xml b/app/src/main/res/menu/speak_engine.xml new file mode 100644 index 000000000..9ca269d05 --- /dev/null +++ b/app/src/main/res/menu/speak_engine.xml @@ -0,0 +1,18 @@ + +

+ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/arrays.xml b/app/src/main/res/values-zh-rHK/arrays.xml index a11c1cb68..346489faf 100644 --- a/app/src/main/res/values-zh-rHK/arrays.xml +++ b/app/src/main/res/values-zh-rHK/arrays.xml @@ -1,21 +1,6 @@ - - 度小美 - 度小宇 - 度逍遙 - 度丫丫 - 度小嬌 - 度米朵 - 度博文 - 度小童 - 度小萌 - 百度騷男 - 百度評書 - 百度主持 - - 跟隨系統 亮色主題 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 7ec110746..b13efd3e8 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -543,7 +543,7 @@ %d 分鐘 自動亮度 %s 按頁朗讀 - 在線朗讀 + 朗讀引擎 背景圖片 背景顏色 文字顏色 @@ -749,4 +749,7 @@ 公众号[开源阅读软件] 阅读记录 阅读时间记录 + 本地TTS + 线程数 + 总阅读时间 diff --git a/app/src/main/res/values-zh-rTW/arrays.xml b/app/src/main/res/values-zh-rTW/arrays.xml index 837da7471..2b5966b66 100644 --- a/app/src/main/res/values-zh-rTW/arrays.xml +++ b/app/src/main/res/values-zh-rTW/arrays.xml @@ -5,21 +5,6 @@ @string/book_type_audio - - 度小美 - 度小宇 - 度逍遙 - 度丫丫 - 度小嬌 - 度米朵 - 度博文 - 度小童 - 度小萌 - 百度騷男 - 百度評書 - 百度主持 - - @string/indent_0 @string/indent_1 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 52dd21635..62c626a3f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -544,7 +544,7 @@ %d分鐘 自動亮度%s 按頁朗讀 - 線上朗讀 + 朗讀引擎 背景圖片 背景顏色 文字顏色 @@ -749,5 +749,8 @@ 公众号[开源阅读软件] 阅读记录 阅读时间记录 + 本地TTS + 线程数 + 总阅读时间 diff --git a/app/src/main/res/values-zh/arrays.xml b/app/src/main/res/values-zh/arrays.xml index 3ac5c3cd4..146b90b4e 100644 --- a/app/src/main/res/values-zh/arrays.xml +++ b/app/src/main/res/values-zh/arrays.xml @@ -5,21 +5,6 @@ @string/book_type_audio - - 度小美 - 度小宇 - 度逍遥 - 度丫丫 - 度小娇 - 度米朵 - 度博文 - 度小童 - 度小萌 - 百度骚男 - 百度评书 - 百度主持 - - @string/indent_0 @string/indent_1 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 5aaae10c1..c6877a404 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -545,7 +545,7 @@ %d分钟 自动亮度%s 按页朗读 - 在线朗读 + 朗读引擎 背景图片 背景颜色 文字颜色 @@ -749,4 +749,7 @@ 公众号[开源阅读软件] 阅读记录 阅读时间记录 + 本地TTS + 线程数 + 总阅读时间 diff --git a/app/src/main/res/values/array_values.xml b/app/src/main/res/values/array_values.xml index bdaf1ef49..a545e3641 100644 --- a/app/src/main/res/values/array_values.xml +++ b/app/src/main/res/values/array_values.xml @@ -26,21 +26,6 @@ 3 - - 0 - 1 - 3 - 4 - 5 - 103 - 106 - 110 - 111 - 11 - 6 - 9 - - 0 1 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 0ff78f4bf..33a99ef3e 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -5,21 +5,6 @@ @string/book_type_audio - - du Xiao Mei - du Xiao Yu - du Xiao Yao - du Ya Ya - du Xiao Jiao - du Mi Duo - du Bo Wen - du Xiao Tong - du Xiao Meng - baidu SaoNan - baidu PingShu - baidu ZhuChi - - @string/indent_0 @string/indent_1 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 507cbc347..bdb8fd0b1 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,6 +3,11 @@ + + + + + @@ -25,6 +30,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c2ed0ecc..88f3b7760 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -547,7 +547,7 @@ %d min Auto-Brightness %s Speak by pages - Online Speech + Speak Engine Background images Background color Text color @@ -750,5 +750,8 @@ 公众号[开源阅读软件] 阅读记录 阅读时间记录 + 本地TTS + 线程数 + 总阅读时间 \ No newline at end of file diff --git a/app/src/main/res/xml/pref_config_aloud.xml b/app/src/main/res/xml/pref_config_aloud.xml index a41fa79b1..95d5193d0 100644 --- a/app/src/main/res/xml/pref_config_aloud.xml +++ b/app/src/main/res/xml/pref_config_aloud.xml @@ -14,18 +14,11 @@ android:key="readAloudByPage" app:iconSpaceReserved="false" /> - - -