Merge pull request #1 from gedoor/master

更新
pull/90/head
M17764017422 6 years ago committed by GitHub
commit 73469af493
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      README.md
  2. 2
      app/src/main/AndroidManifest.xml
  3. 0
      app/src/main/assets/txtTocRule.json
  4. 17
      app/src/main/assets/updateLog.md
  5. 12
      app/src/main/java/io/legado/app/README.md
  6. 12
      app/src/main/java/io/legado/app/base/BaseService.kt
  7. 2
      app/src/main/java/io/legado/app/constant/EventBus.kt
  8. 2
      app/src/main/java/io/legado/app/constant/IntentAction.kt
  9. 12
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  10. 6
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  11. 2
      app/src/main/java/io/legado/app/data/README.md
  12. 8
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  13. 4
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  14. 20
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  15. 12
      app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
  16. 3
      app/src/main/java/io/legado/app/data/dao/RssStarDao.kt
  17. 8
      app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
  18. 20
      app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt
  19. 11
      app/src/main/java/io/legado/app/data/entities/Book.kt
  20. 22
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  21. 7
      app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt
  22. 14
      app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt
  23. 17
      app/src/main/java/io/legado/app/help/AppConfig.kt
  24. 45
      app/src/main/java/io/legado/app/help/BookHelp.kt
  25. 12
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  26. 49
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  27. 55
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  28. 100
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  29. 15
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  30. 5
      app/src/main/java/io/legado/app/lib/theme/TintHelper.kt
  31. 231
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  32. 30
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  33. 2
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  34. 6
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  35. 6
      app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt
  36. 56
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  37. 53
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  38. 63
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  39. 14
      app/src/main/java/io/legado/app/service/DownloadService.kt
  40. 52
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  41. 6
      app/src/main/java/io/legado/app/service/README.md
  42. 10
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  43. 12
      app/src/main/java/io/legado/app/service/WebService.kt
  44. 18
      app/src/main/java/io/legado/app/service/help/AudioPlay.kt
  45. 35
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  46. 4
      app/src/main/java/io/legado/app/service/help/Download.kt
  47. 21
      app/src/main/java/io/legado/app/service/help/ReadAloud.kt
  48. 2
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  49. 15
      app/src/main/java/io/legado/app/ui/README.md
  50. 14
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  51. 4
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  52. 117
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  53. 87
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt
  54. 18
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt
  55. 10
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  56. 7
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  57. 50
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  58. 23
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  59. 7
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  60. 69
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  61. 21
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  62. 12
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  63. 11
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  64. 28
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  65. 52
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt
  66. 19
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  67. 32
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  68. 4
      app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt
  69. 6
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  70. 9
      app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt
  71. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  72. 24
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  73. 47
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  74. 42
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  75. 29
      app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt
  76. 21
      app/src/main/java/io/legado/app/ui/changecover/ChangeCoverDialog.kt
  77. 67
      app/src/main/java/io/legado/app/ui/changecover/ChangeCoverViewModel.kt
  78. 16
      app/src/main/java/io/legado/app/ui/changecover/CoverAdapter.kt
  79. 10
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt
  80. 12
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  81. 4
      app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt
  82. 47
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  83. 51
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  84. 6
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  85. 4
      app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt
  86. 9
      app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt
  87. 2
      app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt
  88. 2
      app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt
  89. 1
      app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt
  90. 40
      app/src/main/java/io/legado/app/ui/filechooser/utils/FileUtils.kt
  91. 16
      app/src/main/java/io/legado/app/ui/importbook/ImportBookViewModel.kt
  92. 11
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  93. 8
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  94. 36
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  95. 56
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  96. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt
  97. 11
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt
  98. 11
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt
  99. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt
  100. 8
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,2 +1,3 @@
# legado # legado
## 阅读3.0 ## 阅读3.0
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg

@ -78,6 +78,7 @@
<activity <activity
android:name=".ui.about.DonateActivity" android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity android:name=".ui.book.arrange.ArrangeBookActivity" />
<activity android:name=".ui.book.info.BookInfoActivity" /> <activity android:name=".ui.book.info.BookInfoActivity" />
<activity android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" /> <activity android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" />
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" /> <activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
@ -103,6 +104,7 @@
<activity android:name=".ui.rss.source.manage.RssSourceActivity" /> <activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" /> <activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" /> <activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.rss.favorites.RssFavoritesActivity" />
<activity android:name=".ui.download.DownloadActivity" /> <activity android:name=".ui.download.DownloadActivity" />
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"

@ -3,8 +3,23 @@
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/02/05**
* 修复bug
* Rss收藏功能完成
* Rss已读标记不会再丢失
**2020/02/04**
* 主界面切换时自动隐藏键盘
* 添加本地书籍完成,解析txt文件完成,本地txt可以看了
* 封面换源,书籍信息界面点击封面弹出封面换源界面
* 默认封面绘制书名和作者
* 修复在线朗读遇到单独标点,停止朗读的问题
**2020/02/02**
* merged commit e584606, rss修复BaseURL模式下部分图片无法加载, 修复可能出现的乱码
* 菜单添加网址功能完成
**2020/01/31** **2020/01/31**
* 正在写本地导入,还没写完
* 修复搜索闪退,因为默认线程为0了 * 修复搜索闪退,因为默认线程为0了
**2020/01/30** **2020/01/30**

@ -0,0 +1,12 @@
## 文件结构介绍
* base 基类
* constant 常量
* data 数据
* help 帮助
* lib 库
* model 解析
* receiver 广播侦听
* service 服务
* ui 界面
* web web服务

@ -3,17 +3,27 @@ package io.legado.app.base
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
abstract class BaseService : Service(), CoroutineScope by MainScope() { abstract class BaseService : Service(), CoroutineScope by MainScope() {
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
cancel() cancel()

@ -1,6 +1,6 @@
package io.legado.app.constant package io.legado.app.constant
object Bus { object EventBus {
const val MEDIA_BUTTON = "mediaButton" const val MEDIA_BUTTON = "mediaButton"
const val RECREATE = "RECREATE" const val RECREATE = "RECREATE"
const val UP_BOOK = "sourceDebugLog" const val UP_BOOK = "sourceDebugLog"

@ -1,6 +1,6 @@
package io.legado.app.constant package io.legado.app.constant
object Action { object IntentAction {
const val start = "start" const val start = "start"
const val play = "play" const val play = "play"
const val stop = "stop" const val stop = "stop"

@ -1,12 +1,17 @@
package io.legado.app.constant package io.legado.app.constant
object PreferKey { object PreferKey {
const val versionCode = "versionCode"
const val themeMode = "themeMode"
const val downloadPath = "downloadPath" const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar" const val hideStatusBar = "hideStatusBar"
const val clickAllNext = "clickAllNext" const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar" const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch" const val precisionSearch = "precisionSearch"
const val readAloudOnLine = "readAloudOnLine"
const val readAloudByPage = "readAloudByPage"
const val ttsSpeechRate = "ttsSpeechRate"
const val ttsSpeechPer = "ttsSpeechPer"
const val prevKey = "prevKeyCode" const val prevKey = "prevKeyCode"
const val nextKey = "nextKeyCode" const val nextKey = "nextKeyCode"
const val showRss = "showRss" const val showRss = "showRss"
@ -21,5 +26,8 @@ object PreferKey {
const val backupPath = "backupUri" const val backupPath = "backupUri"
const val threadCount = "threadCount" const val threadCount = "threadCount"
const val keepLight = "keep_light" const val keepLight = "keep_light"
const val autoDarkMode = "autoDarkMode" const val webService = "webService"
const val webDavUrl = "web_dav_url"
const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password"
} }

@ -16,8 +16,9 @@ import kotlinx.coroutines.launch
@Database( @Database(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssStar::class], RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
version = 6, RssStar::class, TxtTocRule::class],
version = 8,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -50,4 +51,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun rssArticleDao(): RssArticleDao abstract fun rssArticleDao(): RssArticleDao
abstract fun rssStarDao(): RssStarDao abstract fun rssStarDao(): RssStarDao
abstract fun cookieDao(): CookieDao abstract fun cookieDao(): CookieDao
abstract fun txtTocRule(): TxtTocRuleDao
} }

@ -1 +1,3 @@
## 存储数据用 ## 存储数据用
* dao 数据操作
* entities 数据模型

@ -45,7 +45,7 @@ interface BookDao {
val hasUpdateBooks: List<Book> val hasUpdateBooks: List<Book>
@get:Query("SELECT * FROM books") @get:Query("SELECT * FROM books")
val allBooks: List<Book> val all: List<Book>
@get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1") @get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1")
val lastReadBook: Book? val lastReadBook: Book?
@ -60,10 +60,10 @@ interface BookDao {
fun insert(vararg book: Book) fun insert(vararg book: Book)
@Update @Update
fun update(vararg books: Book) fun update(vararg book: Book)
@Query("delete from books where bookUrl = :bookUrl") @Delete
fun delete(bookUrl: String) fun delete(vararg book: Book)
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl") @Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
fun upProgress(bookUrl: String, pos: Int) fun upProgress(bookUrl: String, pos: Int)

@ -19,8 +19,8 @@ interface BookGroupDao {
@get:Query("SELECT MAX(`order`) FROM book_groups") @get:Query("SELECT MAX(`order`) FROM book_groups")
val maxOrder: Int val maxOrder: Int
@Query("SELECT * FROM book_groups ORDER BY `order`") @get:Query("SELECT * FROM book_groups ORDER BY `order`")
fun all(): List<BookGroup> val all: List<BookGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg bookGroup: BookGroup) fun insert(vararg bookGroup: BookGroup)

@ -29,21 +29,6 @@ interface BookSourceDao {
@Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey") @Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
fun searchIsEnable(searchKey: String = ""): List<Boolean> fun searchIsEnable(searchKey: String = ""): List<Boolean>
@Query("update book_sources set enabled = 1 where bookSourceUrl = :sourceUrl")
fun enableSection(sourceUrl: String)
@Query("update book_sources set enabled = 0 where bookSourceUrl = :sourceUrl")
fun disableSection(sourceUrl: String)
@Query("update book_sources set enabledExplore = 1 where bookSourceUrl = :sourceUrl")
fun enableSectionExplore(sourceUrl: String)
@Query("update book_sources set enabledExplore = 0 where bookSourceUrl = :sourceUrl")
fun disableSectionExplore(sourceUrl: String)
@Query("delete from book_sources where bookSourceUrl = :sourceUrl")
fun delSection(sourceUrl: String)
@Query("select * from book_sources where enabledExplore = 1 order by customOrder asc") @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
fun observeFind(): DataSource.Factory<Int, BookSource> fun observeFind(): DataSource.Factory<Int, BookSource>
@ -53,6 +38,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'") @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
fun getEnabledByGroup(group: String): List<BookSource> fun getEnabledByGroup(group: String): List<BookSource>
@get:Query("select * from book_sources where bookUrlPattern is not null || bookUrlPattern <> ''")
val hasBookUrlPattern: List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''") @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
val noGroup: List<BookSource> val noGroup: List<BookSource>
@ -75,7 +63,7 @@ interface BookSourceDao {
fun update(vararg bookSource: BookSource) fun update(vararg bookSource: BookSource)
@Delete @Delete
fun delete(bookSource: BookSource) fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key") @Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String) fun delete(key: String)

@ -3,6 +3,7 @@ package io.legado.app.data.dao
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssReadRecord
@Dao @Dao
interface RssArticleDao { interface RssArticleDao {
@ -10,7 +11,11 @@ interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin and link = :link") @Query("select * from rssArticles where origin = :origin and link = :link")
fun get(origin: String, link: String): RssArticle? fun get(origin: String, link: String): RssArticle?
@Query("select * from rssArticles where origin = :origin order by `order` desc") @Query(
"""select t1.link, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read
from rssArticles as t1 left join rssReadRecords as t2
on t1.link = t2.record where origin = :origin order by `order` desc"""
)
fun liveByOrigin(origin: String): LiveData<List<RssArticle>> fun liveByOrigin(origin: String): LiveData<List<RssArticle>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
@ -24,4 +29,9 @@ interface RssArticleDao {
@Query("delete from rssArticles where origin = :origin") @Query("delete from rssArticles where origin = :origin")
fun delete(origin: String) fun delete(origin: String)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertRecord(vararg rssReadRecord: RssReadRecord)
} }

@ -7,6 +7,9 @@ import io.legado.app.data.entities.RssStar
@Dao @Dao
interface RssStarDao { interface RssStarDao {
@get:Query("select * from rssStars order by starTime desc")
val all: List<RssStar>
@Query("select * from rssStars where origin = :origin and link = :link") @Query("select * from rssStars where origin = :origin and link = :link")
fun get(origin: String, link: String): RssStar? fun get(origin: String, link: String): RssStar?

@ -28,6 +28,14 @@ interface SearchBookDao {
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder") @Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder")
fun getByNameAuthorEnable(name: String, author: String): List<SearchBook> fun getByNameAuthorEnable(name: String, author: String): List<SearchBook>
@Query(
"""select * from searchBooks
where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1)
and coverUrl is not null and coverUrl <> ''
order by originOrder"""
)
fun getEnableHasCover(name: String, author: String): List<SearchBook>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg searchBook: SearchBook): List<Long> fun insert(vararg searchBook: SearchBook): List<Long>

@ -0,0 +1,20 @@
package io.legado.app.data.dao
import androidx.room.*
import io.legado.app.data.entities.TxtTocRule
@Dao
interface TxtTocRuleDao {
@get:Query("select * from txtTocRules order by serialNumber")
val all: List<TxtTocRule>
@get:Query("select * from txtTocRules where enable = 1 order by serialNumber")
val enabled: List<TxtTocRule>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg rule: TxtTocRule)
@Delete
fun delete(vararg rule: TxtTocRule)
}

@ -10,6 +10,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
import kotlin.math.max import kotlin.math.max
@Parcelize @Parcelize
@ -19,7 +20,7 @@ data class Book(
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents) var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local) var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 var originName: String = "", //书源名称 or 本地书籍文件名
var name: String = "", // 书籍名称(书源获取) var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取) var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取) override var kind: String? = null, // 分类信息(书源获取)
@ -52,6 +53,10 @@ data class Book(
return origin == BookType.local return origin == BookType.local
} }
fun isTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true)
}
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null override var variableMap: HashMap<String, String>? = null
@ -81,6 +86,10 @@ data class Book(
variable = GSON.toJson(variableMap) variable = GSON.toJson(variableMap)
} }
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
fun toSearchBook(): SearchBook { fun toSearchBook(): SearchBook {
return SearchBook( return SearchBook(
name = name, name = name,

@ -47,6 +47,18 @@ data class BookSource(
var ruleToc: String? = null, // 目录页规则 var ruleToc: String? = null, // 目录页规则
var ruleContent: String? = null // 正文页规则 var ruleContent: String? = null // 正文页规则
) : Parcelable, JsExtensions { ) : Parcelable, JsExtensions {
override fun hashCode(): Int {
return bookSourceUrl.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is BookSource) {
return other.bookSourceUrl == bookSourceUrl
}
return false
}
@Ignore @Ignore
@IgnoredOnParcel @IgnoredOnParcel
private var searchRuleV: SearchRule? = null private var searchRuleV: SearchRule? = null
@ -126,6 +138,16 @@ data class BookSource(
return contentRuleV!! return contentRuleV!!
} }
fun addGroup(group: String) {
bookSourceGroup?.let {
if (!it.contains(group)) {
bookSourceGroup = "$it;$group"
}
} ?: let {
bookSourceGroup = group
}
}
fun getExploreKinds(): ArrayList<ExploreKind>? { fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>() val exploreKinds = arrayListOf<ExploreKind>()
exploreUrl?.let { exploreUrl?.let {

@ -0,0 +1,7 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "rssReadRecords")
data class RssReadRecord(@PrimaryKey val record: String, val read: Boolean = true)

@ -0,0 +1,14 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "txtTocRules")
data class TxtTocRule(
@PrimaryKey
var name: String = "",
var rule: String = "",
var serialNumber: Int,
var enable: Boolean = true
)

@ -8,7 +8,7 @@ object AppConfig {
var isNightTheme: Boolean var isNightTheme: Boolean
get() { get() {
return when (App.INSTANCE.getPrefString("themeMode", "0")) { return when (App.INSTANCE.getPrefString(PreferKey.themeMode, "0")) {
"1" -> false "1" -> false
"2" -> true "2" -> true
else -> App.INSTANCE.sysIsDarkMode() else -> App.INSTANCE.sysIsDarkMode()
@ -16,9 +16,9 @@ object AppConfig {
} }
set(value) { set(value) {
if (value) { if (value) {
App.INSTANCE.putPrefString("themeMode", "2") App.INSTANCE.putPrefString(PreferKey.themeMode, "2")
} else { } else {
App.INSTANCE.putPrefString("themeMode", "1") App.INSTANCE.putPrefString(PreferKey.themeMode, "1")
} }
} }
@ -50,6 +50,17 @@ object AppConfig {
} }
} }
var ttsSpeechRate: Int
get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value)
}
val ttsSpeechPer: String
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0"
val isEInkMode: Boolean val isEInkMode: Boolean
get() = App.INSTANCE.getPrefBoolean("isEInkMode") get() = App.INSTANCE.getPrefBoolean("isEInkMode")
val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false)
} }

@ -7,6 +7,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.* import io.legado.app.utils.*
import org.apache.commons.text.similarity.JaccardSimilarity import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File import java.io.File
@ -14,7 +15,7 @@ import kotlin.math.min
object BookHelp { object BookHelp {
private const val cacheFolderName = "book_cache" private const val cacheFolderName = "book_cache"
private var downloadPath: String = var downloadPath: String =
App.INSTANCE.getPrefString(PreferKey.downloadPath) App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath ?: App.INSTANCE.cacheDir.absolutePath
@ -37,7 +38,7 @@ object BookHelp {
} }
fun clearCache() { fun clearCache() {
if (downloadUri.isDocumentUri(App.INSTANCE)) { if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri) DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName) ?.findFile(cacheFolderName)
?.delete() ?.delete()
@ -54,17 +55,19 @@ object BookHelp {
@Synchronized @Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) { fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return if (content.isEmpty()) return
if (downloadUri.isDocumentUri(App.INSTANCE)) { if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument( DocumentUtils.getDirDocument(
root, root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.listFiles()?.forEach { )?.uri?.let { uri ->
if (it.name?.startsWith(String.format("%05d", bookChapter.index)) == true) { DocumentUtils.listFiles(App.INSTANCE, uri).forEach {
it.delete() if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
DocumentFile.fromSingleUri(App.INSTANCE, it.uri)?.delete()
return@forEach return@forEach
} }
} }
}
DocumentUtils.createFileIfNotExist( DocumentUtils.createFileIfNotExist(
root, root,
"${bookChapterName(bookChapter)}.nb", "${bookChapterName(bookChapter)}.nb",
@ -90,7 +93,7 @@ object BookHelp {
} }
fun getChapterCount(book: Book): Int { fun getChapterCount(book: Book): Int {
if (downloadUri.isDocumentUri(App.INSTANCE)) { if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.createFolderIfNotExist( return DocumentUtils.createFolderIfNotExist(
root, root,
@ -107,7 +110,11 @@ object BookHelp {
} }
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
if (downloadUri.isDocumentUri(App.INSTANCE)) { when {
book.isLocalBook() -> {
return true
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists( return DocumentUtils.exists(
root, root,
@ -115,18 +122,24 @@ object BookHelp {
subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
) )
} }
} else { }
else -> {
return FileUtils.exists( return FileUtils.exists(
File(downloadPath), File(downloadPath),
"${bookChapterName(bookChapter)}.nb", "${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
) )
} }
}
return false return false
} }
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
if (downloadUri.isDocumentUri(App.INSTANCE)) { when {
book.isLocalBook() -> {
return AnalyzeTxtFile.getContent(book, bookChapter)
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument( return DocumentUtils.getDirDocument(
root, root,
@ -134,7 +147,8 @@ object BookHelp {
)?.findFile("${bookChapterName(bookChapter)}.nb") )?.findFile("${bookChapterName(bookChapter)}.nb")
?.uri?.readText(App.INSTANCE) ?.uri?.readText(App.INSTANCE)
} }
} else { }
else -> {
val file = FileUtils.getFile( val file = FileUtils.getFile(
File(downloadPath), File(downloadPath),
"${bookChapterName(bookChapter)}.nb", "${bookChapterName(bookChapter)}.nb",
@ -144,11 +158,14 @@ object BookHelp {
return file.readText() return file.readText()
} }
} }
}
return null return null
} }
fun delContent(book: Book, bookChapter: BookChapter) { fun delContent(book: Book, bookChapter: BookChapter) {
if (downloadUri.isDocumentUri(App.INSTANCE)) { when {
book.isLocalBook() -> return
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument( DocumentUtils.getDirDocument(
root, root,
@ -156,7 +173,8 @@ object BookHelp {
)?.findFile("${bookChapterName(bookChapter)}.nb") )?.findFile("${bookChapterName(bookChapter)}.nb")
?.delete() ?.delete()
} }
} else { }
else -> {
FileUtils.createFileIfNotExist( FileUtils.createFileIfNotExist(
File(downloadPath), File(downloadPath),
"${bookChapterName(bookChapter)}.nb", "${bookChapterName(bookChapter)}.nb",
@ -164,6 +182,7 @@ object BookHelp {
).delete() ).delete()
} }
} }
}
private fun formatFolderName(folderName: String): String { private fun formatFolderName(folderName: String): String {
return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "") return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "")

@ -12,13 +12,15 @@ import java.io.File
object ImageLoader { object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> { fun load(context: Context, path: String?): RequestBuilder<Drawable> {
if (path?.startsWith("http", true) == true) { return when {
return Glide.with(context).load(path) path.isNullOrEmpty() -> Glide.with(context).load(path)
path.startsWith("http", true) -> Glide.with(context).load(path)
else -> try {
Glide.with(context).load(File(path))
} catch (e: Exception) {
Glide.with(context).load(path)
} }
kotlin.runCatching {
return Glide.with(context).load(File(path))
} }
return Glide.with(context).load(path)
} }
fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> { fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> {

@ -36,6 +36,7 @@ object Backup {
"bookGroup.json", "bookGroup.json",
"bookSource.json", "bookSource.json",
"rssSource.json", "rssSource.json",
"rssStar.json",
"replaceRule.json", "replaceRule.json",
ReadBookConfig.readConfigFileName, ReadBookConfig.readConfigFileName,
"config.xml" "config.xml"
@ -44,41 +45,12 @@ object Backup {
suspend fun backup(context: Context, uri: Uri?) { suspend fun backup(context: Context, uri: Uri?) {
withContext(IO) { withContext(IO) {
App.db.bookDao().allBooks.let { writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
if (it.isNotEmpty()) { writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
val json = GSON.toJson(it) writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
FileUtils.createFileIfNotExist(backupPath + File.separator + "bookshelf.json") writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
.writeText(json) writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
} writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
}
App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.createFileIfNotExist(backupPath + File.separator + "bookGroup.json")
.writeText(json)
}
}
App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.createFileIfNotExist(backupPath + File.separator + "bookSource.json")
.writeText(json)
}
}
App.db.rssSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.createFileIfNotExist(backupPath + File.separator + "rssSource.json")
.writeText(json)
}
}
App.db.replaceRuleDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.createFileIfNotExist(backupPath + File.separator + "replaceRule.json")
.writeText(json)
}
}
GSON.toJson(ReadBookConfig.configList)?.let { GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName) FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it) .writeText(it)
@ -106,6 +78,13 @@ object Backup {
} }
} }
private fun writeListToJson(list: List<Any>, fileName: String, path: String) {
if (list.isNotEmpty()) {
val json = GSON.toJson(list)
FileUtils.createFileIfNotExist(path + File.separator + fileName).writeText(json)
}
}
private fun copyBackup(context: Context, uri: Uri) { private fun copyBackup(context: Context, uri: Uri) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) { for (fileName in backupFileNames) {

@ -0,0 +1,55 @@
package io.legado.app.help.storage
import android.util.Log
import io.legado.app.App
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.Book
import io.legado.app.utils.readBool
import io.legado.app.utils.readInt
import io.legado.app.utils.readLong
import io.legado.app.utils.readString
object OldBook {
fun toNewBook(json: String): List<Book> {
val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao().allBookUrls.toSet()
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
val book = Book()
book.bookUrl = jsonItem.readString("$.noteUrl") ?: ""
if (book.bookUrl.isBlank()) continue
book.name = jsonItem.readString("$.bookInfoBean.name") ?: ""
if (book.bookUrl in existingBooks) {
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}")
continue
}
book.origin = jsonItem.readString("$.tag") ?: ""
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: ""
book.author = jsonItem.readString("$.bookInfoBean.author") ?: ""
book.type =
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl")
book.customCoverUrl = jsonItem.readString("$.customCoverPath")
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0
book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0
book.order = jsonItem.readInt("$.serialNumber") ?: 0
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true
book.variable = jsonItem.readString("$.variable")
books.add(book)
}
return books
}
}

@ -2,17 +2,19 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext import com.jayway.jsonpath.ParseContext
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.* import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -49,52 +51,24 @@ object Restore {
suspend fun restore(path: String) { suspend fun restore(path: String) {
withContext(IO) { withContext(IO) {
try { fileToListT<Book>(path, "bookshelf.json")?.let {
val file = FileUtils.createFileIfNotExist(path + File.separator + "bookshelf.json")
val json = file.readText()
GSON.fromJsonArray<Book>(json)?.let {
App.db.bookDao().insert(*it.toTypedArray()) App.db.bookDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) { fileToListT<BookGroup>(path, "bookGroup.json")?.let {
e.printStackTrace()
}
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + "bookGroup.json")
val json = file.readText()
GSON.fromJsonArray<BookGroup>(json)?.let {
App.db.bookGroupDao().insert(*it.toTypedArray()) App.db.bookGroupDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) { fileToListT<BookSource>(path, "bookSource.json")?.let {
e.printStackTrace()
}
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + "bookSource.json")
val json = file.readText()
GSON.fromJsonArray<BookSource>(json)?.let {
App.db.bookSourceDao().insert(*it.toTypedArray()) App.db.bookSourceDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) { fileToListT<RssSource>(path, "rssSource.json")?.let {
e.printStackTrace()
}
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + "rssSource.json")
val json = file.readText()
GSON.fromJsonArray<RssSource>(json)?.let {
App.db.rssSourceDao().insert(*it.toTypedArray()) App.db.rssSourceDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) { fileToListT<RssStar>(path, "rssStar.json")?.let {
e.printStackTrace() App.db.rssStarDao().insert(*it.toTypedArray())
} }
try { fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
val file =
FileUtils.createFileIfNotExist(path + File.separator + "replaceRule.json")
val json = file.readText()
GSON.fromJsonArray<ReplaceRule>(json)?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray()) App.db.replaceRuleDao().insert(*it.toTypedArray())
} }
} catch (e: Exception) {
e.printStackTrace()
}
try { try {
val file = val file =
FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.readConfigFileName) FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.readConfigFileName)
@ -117,11 +91,23 @@ object Restore {
is String -> edit.putString(it.key, value) is String -> edit.putString(it.key, value)
else -> Unit else -> Unit
} }
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
edit.commit() edit.commit()
} }
} }
} }
private inline fun <reified T> fileToListT(path: String, fileName: String): List<T>? {
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
val json = file.readText()
return GSON.fromJsonArray(json)
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
fun importYueDuData(context: Context) { fun importYueDuData(context: Context) {
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
try {// 导入书架 try {// 导入书架
@ -169,43 +155,7 @@ object Restore {
} }
fun importOldBookshelf(json: String): Int { fun importOldBookshelf(json: String): Int {
val books = mutableListOf<Book>() val books = OldBook.toNewBook(json)
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao().allBookUrls.toSet()
for (item in items) {
val jsonItem = jsonPath.parse(item)
val book = Book()
book.bookUrl = jsonItem.readString("$.noteUrl") ?: ""
if (book.bookUrl.isBlank()) continue
book.name = jsonItem.readString("$.bookInfoBean.name") ?: ""
if (book.bookUrl in existingBooks) {
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}")
continue
}
book.origin = jsonItem.readString("$.tag") ?: ""
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: ""
book.author = jsonItem.readString("$.bookInfoBean.author") ?: ""
book.type =
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl")
book.customCoverUrl = jsonItem.readString("$.customCoverPath")
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0
book.durChapterTitle = jsonItem.readString("$.durChapterName")
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0
book.group = jsonItem.readInt("$.group") ?: 0
book.intro = jsonItem.readString("$.bookInfoBean.introduce")
book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0
book.order = jsonItem.readInt("$.serialNumber") ?: 0
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true
book.variable = jsonItem.readString("$.variable")
books.add(book)
}
App.db.bookDao().insert(*books.toTypedArray()) App.db.bookDao().insert(*books.toTypedArray())
return books.size return books.size
} }

@ -2,6 +2,7 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.PreferKey
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth import io.legado.app.lib.webdav.http.HttpAuth
@ -23,15 +24,15 @@ object WebDavHelp {
} }
private fun getWebDavUrl(): String? { private fun getWebDavUrl(): String? {
var url = App.INSTANCE.getPrefString("web_dav_url") var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl)
if (url.isNullOrBlank()) return null if (url.isNullOrBlank()) return null
if (!url.endsWith("/")) url += "/" if (!url.endsWith("/")) url += "/"
return url return url
} }
private fun initWebDav(): Boolean { private fun initWebDav(): Boolean {
val account = App.INSTANCE.getPrefString("web_dav_account") val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount)
val password = App.INSTANCE.getPrefString("web_dav_password") val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword)
if (!account.isNullOrBlank() && !password.isNullOrBlank()) { if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password) HttpAuth.auth = HttpAuth.Auth(account, password)
return true return true
@ -54,12 +55,12 @@ object WebDavHelp {
return names return names
} }
suspend fun showRestoreDialog(context: Context): Boolean { suspend fun showRestoreDialog(context: Context, restoreSuccess: () -> Unit): Boolean {
val names = withContext(IO) { getWebDavFileNames() } val names = withContext(IO) { getWebDavFileNames() }
return if (names.isNotEmpty()) { return if (names.isNotEmpty()) {
context.selector(title = "选择恢复文件", items = names) { _, index -> context.selector(title = "选择恢复文件", items = names) { _, index ->
if (index in 0 until names.size) { if (index in 0 until names.size) {
restoreWebDav(names[index]) restoreWebDav(names[index], restoreSuccess)
} }
} }
true true
@ -68,7 +69,7 @@ object WebDavHelp {
} }
} }
private fun restoreWebDav(name: String) { private fun restoreWebDav(name: String, success: () -> Unit) {
Coroutine.async { Coroutine.async {
getWebDavUrl()?.let { getWebDavUrl()?.let {
val file = WebDav(it + "legado/" + name) val file = WebDav(it + "legado/" + name)
@ -77,6 +78,8 @@ object WebDavHelp {
ZipUtils.unzipFile(zipFilePath, unzipFilesPath) ZipUtils.unzipFile(zipFilePath, unzipFilesPath)
Restore.restore(unzipFilesPath) Restore.restore(unzipFilesPath)
} }
}.onSuccess {
success.invoke()
} }
} }

@ -254,11 +254,10 @@ object TintHelper {
if (!skipIndeterminate) if (!skipIndeterminate)
progressBar.indeterminateTintList = sl progressBar.indeterminateTintList = sl
} else { } else {
val mode = PorterDuff.Mode.SRC_IN
if (!skipIndeterminate && progressBar.indeterminateDrawable != null) if (!skipIndeterminate && progressBar.indeterminateDrawable != null)
progressBar.indeterminateDrawable.setColorFilter(color, mode) progressBar.indeterminateDrawable.setTint(color)
if (progressBar.progressDrawable != null) if (progressBar.progressDrawable != null)
progressBar.progressDrawable.setColorFilter(color, mode) progressBar.progressDrawable.setTint(color)
} }
} }

@ -2,20 +2,239 @@ package io.legado.app.model.localBook
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import io.legado.app.App
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.utils.EncodingDetect import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.utils.*
import java.io.File
import java.io.RandomAccessFile
import java.nio.charset.Charset
import java.util.regex.Matcher
import java.util.regex.Pattern
object AnalyzeTxtFile { object AnalyzeTxtFile {
private const val folderName = "bookTxt"
private const val BLANK: Byte = 0x0a
//默认从文件中获取数据的长度
private const val BUFFER_SIZE = 512 * 1024
//没有标题的时候,每个章节的最大长度
private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024
private val cacheFolder: File by lazy {
val rootFile = App.INSTANCE.getExternalFilesDir(null)
?: App.INSTANCE.externalCacheDir
?: App.INSTANCE.cacheDir
FileUtils.createFileIfNotExist(rootFile, subDirs = *arrayOf(folderName))
}
fun analyze(context: Context, book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(context, book)
book.charset = EncodingDetect.getEncode(bookFile)
val charset = book.fileCharset()
val toc = arrayListOf<BookChapter>()
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
val rulePattern = getTocRule(book, bookStream, charset)
//加载章节
val buffer = ByteArray(BUFFER_SIZE)
//获取到的块起始点,在文件中的位置
var curOffset: Long = 0
//block的个数
var blockPos = 0
//读取的长度
var length: Int
var allLength = 0
//获取文件中的数据到buffer,直到没有数据为止
while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) {
++blockPos
//如果存在Chapter
if (rulePattern != null) { //将数据转换成String
var blockContent = String(buffer, 0, length, charset)
val lastN = blockContent.lastIndexOf("\n")
if (lastN != 0) {
blockContent = blockContent.substring(0, lastN)
length = blockContent.toByteArray(charset).size
allLength += length
bookStream.seek(allLength.toLong())
}
//当前Block下使过的String的指针
var seekPos = 0
//进行正则匹配
val matcher: Matcher = rulePattern.matcher(blockContent)
//如果存在相应章节
while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置
val chapterStart = matcher.start()
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容
val chapterContent = blockContent.substring(seekPos, chapterStart)
//设置指针偏移
seekPos += chapterContent.length
if (toc.size == 0) { //如果当前没有章节,那么就是序章
//加入简介
book.intro = chapterContent
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = chapterContent.toByteArray(charset).size.toLong()
toc.add(curChapter)
} else { //否则就block分割之后,上一个章节的剩余内容
//获取上一章节
val lastChapter = toc.last()
//将当前段落添加上一章去
lastChapter.end =
lastChapter.end!! + chapterContent.toByteArray(charset).size
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = lastChapter.end
toc.add(curChapter)
}
} else { //是否存在章节
if (toc.size != 0) { //获取章节内容
val chapterContent = blockContent.substring(seekPos, matcher.start())
seekPos += chapterContent.length
//获取上一章节
val lastChapter = toc.last()
lastChapter.end =
lastChapter.start!! + chapterContent.toByteArray(charset).size
//创建当前章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = lastChapter.end
toc.add(curChapter)
} else { //如果章节不存在则创建章节
val curChapter = BookChapter()
curChapter.title = matcher.group()
curChapter.start = 0L
curChapter.end = 0L
toc.add(curChapter)
}
}
}
} else { //进行本地虚拟分章
//章节在buffer的偏移量
var chapterOffset = 0
//当前剩余可分配的长度
var strLength = length
//分章的位置
var chapterPos = 0
while (strLength > 0) {
++chapterPos
//是否长度超过一章
if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { //在buffer中一章的终止点
var end = length
//寻找换行符作为终止点
for (i in chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER until length) {
if (buffer[i] == BLANK) {
end = i
break
}
}
val chapter = BookChapter()
chapter.title = "${blockPos}章($chapterPos)"
chapter.start = curOffset + chapterOffset + 1
chapter.end = curOffset + end
toc.add(chapter)
//减去已经被分配的长度
strLength -= (end - chapterOffset)
//设置偏移的位置
chapterOffset = end
} else {
val chapter = BookChapter()
chapter.title = "" + blockPos + "" + "(" + chapterPos + ")"
chapter.start = curOffset + chapterOffset + 1
chapter.end = curOffset + length
toc.add(chapter)
strLength = 0
}
}
}
fun analyze(context: Context, book: Book) { //block的偏移点
context.contentResolver.openInputStream(Uri.parse(book.bookUrl))?.use { stream -> curOffset += length.toLong()
val rawByteArray = ByteArray(2000)
stream.read(rawByteArray)
book.charset = EncodingDetect.getJavaEncode(rawByteArray)
if (rulePattern != null) { //设置上一章的结尾
val lastChapter = toc.last()
lastChapter.end = curOffset
}
//当添加的block太多的时候,执行GC
//当添加的block太多的时候,执行GC
if (blockPos % 15 == 0) {
System.gc()
System.runFinalization()
} }
} }
bookStream.close()
for (i in toc.indices) {
val bean = toc[i]
bean.index = i
bean.bookUrl = book.bookUrl
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "")
}
book.latestChapterTitle = toc.last().title
System.gc()
System.runFinalization()
return toc
}
fun getContent(book: Book, bookChapter: BookChapter): String {
val bookFile = getBookFile(App.INSTANCE, book)
//获取文件流
val bookStream = RandomAccessFile(bookFile, "r")
bookStream.seek(bookChapter.start ?: 0)
val extent = (bookChapter.end!! - bookChapter.start!!).toInt()
val content = ByteArray(extent)
bookStream.read(content, 0, extent)
return String(content, book.fileCharset())
}
private fun getBookFile(context: Context, book: Book): File {
val uri = Uri.parse(book.bookUrl)
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf())
if (!bookFile.exists()) {
bookFile.createNewFile()
DocumentUtils.readBytes(context, uri)?.let {
bookFile.writeBytes(it)
}
}
return bookFile
}
private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? {
val tocRules = getTocRules()
var rulePattern: Pattern? = null
//首先获取128k的数据
val buffer = ByteArray(BUFFER_SIZE / 4)
val length = bookStream.read(buffer, 0, buffer.size)
val content = String(buffer, 0, length, charset)
for (tocRule in tocRules) {
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE)
val matcher = pattern.matcher(content)
if (matcher.find()) {
book.tocUrl = tocRule.rule
rulePattern = pattern
break
}
}
bookStream.seek(0)
return rulePattern
}
private fun getTocRules(): List<TxtTocRule> {
val rules = App.db.txtTocRule().all
if (rules.isEmpty()) {
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray ->
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
return it
}
}
}
return rules
}
} }

@ -0,0 +1,30 @@
package io.legado.app.model.localBook
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.data.entities.Book
object LocalBook {
fun importFile(doc: DocumentFile) {
doc.name?.let { fileName ->
val str = fileName.substringBeforeLast(".")
var name = str.substringBefore("作者")
val author = str.substringAfter("作者", "")
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
}
val book = Book(
bookUrl = doc.uri.toString(),
name = name,
author = author,
originName = fileName
)
App.db.bookDao().insert(book)
}
}
}

@ -100,6 +100,8 @@ object BookChapterList {
item.index = index item.index = index
} }
book.latestChapterTitle = chapterList.last().title book.latestChapterTitle = chapterList.last().title
book.durChapterTitle =
chapterList.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle
if (book.totalChapterNum < chapterList.size) { if (book.totalChapterNum < chapterList.size) {
book.lastCheckCount = chapterList.size - book.totalChapterNum book.lastCheckCount = chapterList.size - book.totalChapterNum
} }

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.KeyEvent import android.view.KeyEvent
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.ActivityHelp import io.legado.app.help.ActivityHelp
import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.audio.AudioPlayActivity
@ -51,9 +51,9 @@ class MediaButtonReceiver : BroadcastReceiver() {
private fun readAloud(context: Context) { private fun readAloud(context: Context) {
when { when {
ActivityHelp.isExist(AudioPlayActivity::class.java) -> ActivityHelp.isExist(AudioPlayActivity::class.java) ->
postEvent(Bus.MEDIA_BUTTON, true) postEvent(EventBus.MEDIA_BUTTON, true)
ActivityHelp.isExist(ReadBookActivity::class.java) -> ActivityHelp.isExist(ReadBookActivity::class.java) ->
postEvent(Bus.MEDIA_BUTTON, true) postEvent(EventBus.MEDIA_BUTTON, true)
else -> { else -> {
GlobalScope.launch(Main) { GlobalScope.launch(Main) {
val lastBook: Book? = withContext(IO) { val lastBook: Book? = withContext(IO) {

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.BatteryManager import android.os.BatteryManager
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -28,11 +28,11 @@ class TimeElectricityReceiver : BroadcastReceiver() {
intent?.action?.let { intent?.action?.let {
when (it) { when (it) {
Intent.ACTION_TIME_TICK -> { Intent.ACTION_TIME_TICK -> {
postEvent(Bus.TIME_CHANGED, "") postEvent(EventBus.TIME_CHANGED, "")
} }
Intent.ACTION_BATTERY_CHANGED -> { Intent.ACTION_BATTERY_CHANGED -> {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
postEvent(Bus.BATTERY_CHANGED, level) postEvent(EventBus.BATTERY_CHANGED, level)
} }
} }
} }

@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -81,21 +81,21 @@ class AudioPlayService : BaseService(),
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action -> intent?.action?.let { action ->
when (action) { when (action) {
Action.play -> { IntentAction.play -> {
AudioPlay.book?.let { AudioPlay.book?.let {
title = it.name title = it.name
position = it.durChapterPos position = it.durChapterPos
loadContent(it.durChapterIndex) loadContent(it.durChapterIndex)
} }
} }
Action.pause -> pause(true) IntentAction.pause -> pause(true)
Action.resume -> resume() IntentAction.resume -> resume()
Action.prev -> moveToPrev() IntentAction.prev -> moveToPrev()
Action.next -> moveToNext() IntentAction.next -> moveToNext()
Action.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
Action.addTimer -> addTimer() IntentAction.addTimer -> addTimer()
Action.setTimer -> setTimer(intent.getIntExtra("minute", 0)) IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
Action.adjustProgress -> adjustProgress(intent.getIntExtra("position", position)) IntentAction.adjustProgress -> adjustProgress(intent.getIntExtra("position", position))
else -> stopSelf() else -> stopSelf()
} }
} }
@ -112,7 +112,7 @@ class AudioPlayService : BaseService(),
unregisterReceiver(broadcastReceiver) unregisterReceiver(broadcastReceiver)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
AudioPlay.status = Status.STOP AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP) postEvent(EventBus.AUDIO_STATE, Status.STOP)
} }
private fun play() { private fun play() {
@ -120,7 +120,7 @@ class AudioPlayService : BaseService(),
if (requestFocus()) { if (requestFocus()) {
try { try {
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_STATE, Status.PLAY)
mediaPlayer.reset() mediaPlayer.reset()
val analyzeUrl = val analyzeUrl =
AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true) AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true)
@ -146,7 +146,7 @@ class AudioPlayService : BaseService(),
mediaPlayer.pause() mediaPlayer.pause()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
AudioPlay.status = Status.PAUSE AudioPlay.status = Status.PAUSE
postEvent(Bus.AUDIO_STATE, Status.PAUSE) postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
upNotification() upNotification()
} }
} }
@ -159,7 +159,7 @@ class AudioPlayService : BaseService(),
handler.postDelayed(mpRunnable, 1000) handler.postDelayed(mpRunnable, 1000)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification() upNotification()
} }
@ -178,7 +178,7 @@ class AudioPlayService : BaseService(),
if (isPlaying) { if (isPlaying) {
playbackParams = playbackParams.apply { speed += adjust } playbackParams = playbackParams.apply { speed += adjust }
} }
postEvent(Bus.AUDIO_SPEED, playbackParams.speed) postEvent(EventBus.AUDIO_SPEED, playbackParams.speed)
} }
} }
} }
@ -191,7 +191,7 @@ class AudioPlayService : BaseService(),
if (pause) return if (pause) return
mediaPlayer.start() mediaPlayer.start()
mediaPlayer.seekTo(position) mediaPlayer.seekTo(position)
postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration) postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration)
bookChapter?.let { bookChapter?.let {
it.end = mediaPlayer.duration.toLong() it.end = mediaPlayer.duration.toLong()
} }
@ -205,7 +205,7 @@ class AudioPlayService : BaseService(),
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
if (!mediaPlayer.isPlaying) { if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP) postEvent(EventBus.AUDIO_STATE, Status.STOP)
launch { toast("error: $what $extra $url") } launch { toast("error: $what $extra $url") }
} }
return true return true
@ -238,7 +238,7 @@ class AudioPlayService : BaseService(),
handler.removeCallbacks(dsRunnable) handler.removeCallbacks(dsRunnable)
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(EventBus.TTS_DS, timeMinute)
upNotification() upNotification()
} }
@ -247,7 +247,7 @@ class AudioPlayService : BaseService(),
*/ */
private fun upPlayProgress() { private fun upPlayProgress() {
saveProgress() saveProgress()
postEvent(Bus.AUDIO_PROGRESS, mediaPlayer.currentPosition) postEvent(EventBus.AUDIO_PROGRESS, mediaPlayer.currentPosition)
handler.postDelayed(mpRunnable, 1000) handler.postDelayed(mpRunnable, 1000)
} }
@ -260,9 +260,9 @@ class AudioPlayService : BaseService(),
if (index == AudioPlay.durChapterIndex) { if (index == AudioPlay.durChapterIndex) {
bookChapter = chapter bookChapter = chapter
subtitle = chapter.title subtitle = chapter.title
postEvent(Bus.AUDIO_SUB_TITLE, subtitle) postEvent(EventBus.AUDIO_SUB_TITLE, subtitle)
postEvent(Bus.AUDIO_SIZE, chapter.end?.toInt() ?: 0) postEvent(EventBus.AUDIO_SIZE, chapter.end?.toInt() ?: 0)
postEvent(Bus.AUDIO_PROGRESS, position) postEvent(EventBus.AUDIO_PROGRESS, position)
} }
loadContent(chapter) loadContent(chapter)
} ?: removeLoading(index) } ?: removeLoading(index)
@ -376,7 +376,7 @@ class AudioPlayService : BaseService(),
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(EventBus.TTS_DS, timeMinute)
upNotification() upNotification()
} }
@ -485,24 +485,24 @@ class AudioPlayService : BaseService(),
builder.addAction( builder.addAction(
R.drawable.ic_play_24dp, R.drawable.ic_play_24dp,
getString(R.string.resume), getString(R.string.resume),
thisPendingIntent(Action.resume) thisPendingIntent(IntentAction.resume)
) )
} else { } else {
builder.addAction( builder.addAction(
R.drawable.ic_pause_24dp, R.drawable.ic_pause_24dp,
getString(R.string.pause), getString(R.string.pause),
thisPendingIntent(Action.pause) thisPendingIntent(IntentAction.pause)
) )
} }
builder.addAction( builder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.stop), getString(R.string.stop),
thisPendingIntent(Action.stop) thisPendingIntent(IntentAction.stop)
) )
builder.addAction( builder.addAction(
R.drawable.ic_time_add_24dp, R.drawable.ic_time_add_24dp,
getString(R.string.set_timer), getString(R.string.set_timer),
thisPendingIntent(Action.addTimer) thisPendingIntent(IntentAction.addTimer)
) )
builder.setStyle( builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()

@ -16,10 +16,7 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.*
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.constant.Status
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
@ -43,18 +40,18 @@ abstract class BaseReadAloudService : BaseService(),
} }
} }
private val handler = Handler() internal val handler = Handler()
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
private var mFocusRequest: AudioFocusRequest? = null private var mFocusRequest: AudioFocusRequest? = null
private var broadcastReceiver: BroadcastReceiver? = null private var broadcastReceiver: BroadcastReceiver? = null
private var mediaSessionCompat: MediaSessionCompat? = null private var mediaSessionCompat: MediaSessionCompat? = null
private var title: String = "" private var title: String = ""
private var subtitle: String = "" private var subtitle: String = ""
val contentList = arrayListOf<String>() internal val contentList = arrayListOf<String>()
var nowSpeak: Int = 0 internal var nowSpeak: Int = 0
var readAloudNumber: Int = 0 internal var readAloudNumber: Int = 0
var textChapter: TextChapter? = null internal var textChapter: TextChapter? = null
var pageIndex = 0 internal var pageIndex = 0
private val dsRunnable: Runnable = Runnable { doDs() } private val dsRunnable: Runnable = Runnable { doDs() }
override fun onCreate() { override fun onCreate() {
@ -73,7 +70,7 @@ abstract class BaseReadAloudService : BaseService(),
isRun = false isRun = false
pause = true pause = true
unregisterReceiver(broadcastReceiver) unregisterReceiver(broadcastReceiver)
postEvent(Bus.ALOUD_STATE, Status.STOP) postEvent(EventBus.ALOUD_STATE, Status.STOP)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
mediaSessionCompat?.release() mediaSessionCompat?.release()
} }
@ -81,7 +78,7 @@ abstract class BaseReadAloudService : BaseService(),
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action -> intent?.action?.let { action ->
when (action) { when (action) {
Action.play -> { IntentAction.play -> {
title = intent.getStringExtra("title") ?: "" title = intent.getStringExtra("title") ?: ""
subtitle = intent.getStringExtra("subtitle") ?: "" subtitle = intent.getStringExtra("subtitle") ?: ""
pageIndex = intent.getIntExtra("pageIndex", 0) pageIndex = intent.getIntExtra("pageIndex", 0)
@ -90,13 +87,13 @@ abstract class BaseReadAloudService : BaseService(),
intent.getBooleanExtra("play", true) intent.getBooleanExtra("play", true)
) )
} }
Action.pause -> pauseReadAloud(true) IntentAction.pause -> pauseReadAloud(true)
Action.resume -> resumeReadAloud() IntentAction.resume -> resumeReadAloud()
Action.upTtsSpeechRate -> upSpeechRate(true) IntentAction.upTtsSpeechRate -> upSpeechRate(true)
Action.prevParagraph -> prevP() IntentAction.prevParagraph -> prevP()
Action.nextParagraph -> nextP() IntentAction.nextParagraph -> nextP()
Action.addTimer -> addTimer() IntentAction.addTimer -> addTimer()
Action.setTimer -> setTimer(intent.getIntExtra("minute", 0)) IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
else -> stopSelf() else -> stopSelf()
} }
} }
@ -111,7 +108,7 @@ abstract class BaseReadAloudService : BaseService(),
nowSpeak = 0 nowSpeak = 0
readAloudNumber = textChapter.getReadLength(pageIndex) readAloudNumber = textChapter.getReadLength(pageIndex)
contentList.clear() contentList.clear()
if (getPrefBoolean("readAloudByPage")) { if (getPrefBoolean(PreferKey.readAloudByPage)) {
for (index in pageIndex..textChapter.lastIndex()) { for (index in pageIndex..textChapter.lastIndex()) {
textChapter.page(index)?.text?.split("\n")?.let { textChapter.page(index)?.text?.split("\n")?.let {
contentList.addAll(it) contentList.addAll(it)
@ -127,13 +124,13 @@ abstract class BaseReadAloudService : BaseService(),
open fun play() { open fun play() {
pause = false pause = false
postEvent(Bus.ALOUD_STATE, Status.PLAY) postEvent(EventBus.ALOUD_STATE, Status.PLAY)
upNotification() upNotification()
} }
@CallSuper @CallSuper
open fun pauseReadAloud(pause: Boolean) { open fun pauseReadAloud(pause: Boolean) {
postEvent(Bus.ALOUD_STATE, Status.PAUSE) postEvent(EventBus.ALOUD_STATE, Status.PAUSE)
BaseReadAloudService.pause = pause BaseReadAloudService.pause = pause
upNotification() upNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
@ -170,7 +167,7 @@ abstract class BaseReadAloudService : BaseService(),
handler.removeCallbacks(dsRunnable) handler.removeCallbacks(dsRunnable)
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(EventBus.TTS_DS, timeMinute)
upNotification() upNotification()
} }
@ -186,7 +183,7 @@ abstract class BaseReadAloudService : BaseService(),
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(EventBus.TTS_DS, timeMinute)
upNotification() upNotification()
} }
@ -301,24 +298,24 @@ abstract class BaseReadAloudService : BaseService(),
builder.addAction( builder.addAction(
R.drawable.ic_play_24dp, R.drawable.ic_play_24dp,
getString(R.string.resume), getString(R.string.resume),
aloudServicePendingIntent(Action.resume) aloudServicePendingIntent(IntentAction.resume)
) )
} else { } else {
builder.addAction( builder.addAction(
R.drawable.ic_pause_24dp, R.drawable.ic_pause_24dp,
getString(R.string.pause), getString(R.string.pause),
aloudServicePendingIntent(Action.pause) aloudServicePendingIntent(IntentAction.pause)
) )
} }
builder.addAction( builder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.stop), getString(R.string.stop),
aloudServicePendingIntent(Action.stop) aloudServicePendingIntent(IntentAction.stop)
) )
builder.addAction( builder.addAction(
R.drawable.ic_time_add_24dp, R.drawable.ic_time_add_24dp,
getString(R.string.set_timer), getString(R.string.set_timer),
aloudServicePendingIntent(Action.addTimer) aloudServicePendingIntent(IntentAction.addTimer)
) )
builder.setStyle( builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle() androidx.media.app.NotificationCompat.MediaStyle()

@ -2,28 +2,77 @@ package io.legado.app.service
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.entities.BookSource import io.legado.app.constant.IntentAction
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private var searchPool =
private var sourceList: List<BookSource>? = null Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var idsCount = 0
private val unCheckIds = LinkedHashSet<String>()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
updateNotification(0, getString(R.string.start))
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentAction.start -> intent.getStringArrayListExtra("selectIds")?.let {
check(it)
}
else -> stopSelf()
}
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
task?.cancel()
searchPool.close()
}
private fun check(ids: List<String>) {
task?.cancel()
unCheckIds.clear()
idsCount = ids.size
unCheckIds.addAll(ids)
updateNotification(0, getString(R.string.progress_show, 0, idsCount))
task = execute {
unCheckIds.forEach { sourceUrl ->
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool)
.onError(IO) {
source.addGroup("失效")
App.db.bookSourceDao().update(source)
}.onFinally {
unCheckIds.remove(sourceUrl)
val checkedCount = idsCount - unCheckIds.size
updateNotification(
checkedCount,
getString(R.string.progress_show, checkedCount, idsCount)
)
}
}
}
}
task?.invokeOnCompletion {
stopSelf()
}
} }
/** /**
@ -41,11 +90,9 @@ class CheckSourceService : BaseService() {
.addAction( .addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.cancel), getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, Action.stop) IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
) )
sourceList?.let { builder.setProgress(idsCount, state, false)
builder.setProgress(it.size, state, false)
}
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build() val notification = builder.build()
startForeground(112202, notification) startForeground(112202, notification)

@ -6,9 +6,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
@ -34,7 +34,7 @@ class DownloadService : BaseService() {
builder.addAction( builder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.cancel), getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>(this, Action.stop) IntentHelp.servicePendingIntent<DownloadService>(this, IntentAction.stop)
) )
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
} }
@ -48,12 +48,12 @@ class DownloadService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.action?.let { action -> intent?.action?.let { action ->
when (action) { when (action) {
Action.start -> download( IntentAction.start -> download(
intent.getStringExtra("bookUrl"), intent.getStringExtra("bookUrl"),
intent.getIntExtra("start", 0), intent.getIntExtra("start", 0),
intent.getIntExtra("end", 0) intent.getIntExtra("end", 0)
) )
Action.stop -> stopDownload() IntentAction.stop -> stopDownload()
} }
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
@ -64,7 +64,7 @@ class DownloadService : BaseService() {
searchPool.close() searchPool.close()
handler.removeCallbacks(runnable) handler.removeCallbacks(runnable)
super.onDestroy() super.onDestroy()
postEvent(Bus.UP_DOWNLOAD, false) postEvent(EventBus.UP_DOWNLOAD, false)
} }
private fun download(bookUrl: String?, start: Int, end: Int) { private fun download(bookUrl: String?, start: Int, end: Int) {
@ -105,7 +105,7 @@ class DownloadService : BaseService() {
private fun upDownload() { private fun upDownload() {
updateNotification(notificationContent) updateNotification(notificationContent)
postEvent(Bus.UP_DOWNLOAD, true) postEvent(EventBus.UP_DOWNLOAD, true)
handler.removeCallbacks(runnable) handler.removeCallbacks(runnable)
handler.postDelayed(runnable, 1000) handler.postDelayed(runnable, 1000)
} }

@ -2,12 +2,15 @@ package io.legado.app.service
import android.app.PendingIntent import android.app.PendingIntent
import android.media.MediaPlayer import android.media.MediaPlayer
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.api.HttpPostApi import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.* import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -37,6 +40,7 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job?.cancel()
mediaPlayer.release() mediaPlayer.release()
} }
@ -88,11 +92,12 @@ class HttpReadAloudService : BaseReadAloudService(),
@Synchronized @Synchronized
private fun playAudio(fd: FileDescriptor) { private fun playAudio(fd: FileDescriptor) {
if (playingIndex != nowSpeak && requestFocus()) { if (playingIndex != nowSpeak && requestFocus()) {
playingIndex = nowSpeak
try { try {
mediaPlayer.reset() mediaPlayer.reset()
mediaPlayer.setDataSource(fd) mediaPlayer.setDataSource(fd)
mediaPlayer.prepareAsync() mediaPlayer.prepareAsync()
playingIndex = nowSpeak
postEvent(EventBus.TTS_START, readAloudNumber + 1)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -106,8 +111,8 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun getAudioBody(content: String): Map<String, String> { private fun getAudioBody(content: String): Map<String, String> {
return mapOf( return mapOf(
Pair("tex", encodeTwo(content)), Pair("tex", encodeTwo(content)),
Pair("spd", ((getPrefInt("ttsSpeechRate", 25) + 5) / 5).toString()), Pair("spd", ((AppConfig.ttsSpeechRate + 5) / 10 + 4).toString()),
Pair("per", getPrefString("ttsSpeechPer") ?: "0"), Pair("per", AppConfig.ttsSpeechPer),
Pair("cuid", "baidu_speech_demo"), Pair("cuid", "baidu_speech_demo"),
Pair("idx", "1"), Pair("idx", "1"),
Pair("cod", "2"), Pair("cod", "2"),
@ -135,20 +140,26 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun resumeReadAloud() { override fun resumeReadAloud() {
super.resumeReadAloud() super.resumeReadAloud()
if (playingIndex == -1) {
play()
} else {
mediaPlayer.start() mediaPlayer.start()
} }
}
/**
* 更新朗读速度
*/
override fun upSpeechRate(reset: Boolean) { override fun upSpeechRate(reset: Boolean) {
job?.cancel() job?.cancel()
mediaPlayer.reset() mediaPlayer.stop()
for (i in 0 until nowSpeak) {
contentList.removeAt(0)
}
nowSpeak = 0
playingIndex = -1 playingIndex = -1
play() downloadAudio()
} }
/**
* 上一段
*/
override fun prevP() { override fun prevP() {
if (nowSpeak > 0) { if (nowSpeak > 0) {
mediaPlayer.stop() mediaPlayer.stop()
@ -158,6 +169,9 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
/**
* 下一段
*/
override fun nextP() { override fun nextP() {
if (nowSpeak < contentList.size - 1) { if (nowSpeak < contentList.size - 1) {
mediaPlayer.stop() mediaPlayer.stop()
@ -177,15 +191,27 @@ class HttpReadAloudService : BaseReadAloudService(),
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
} }
} }
postEvent(Bus.TTS_START, readAloudNumber + 1) postEvent(EventBus.TTS_START, readAloudNumber + 1)
} }
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
LogUtils.d("mp", "what:$what extra:$extra")
if (what == -38 && extra == 0) {
return true
}
handler.postDelayed({
readAloudNumber += contentList[nowSpeak].length + 1
if (nowSpeak < contentList.lastIndex) {
nowSpeak++
play()
} else {
nextChapter()
}
}, 1000)
return true return true
} }
override fun onCompletion(mp: MediaPlayer?) { override fun onCompletion(mp: MediaPlayer?) {
LogUtils.d("播放完成", contentList[nowSpeak])
readAloudNumber += contentList[nowSpeak].length + 1 readAloudNumber += contentList[nowSpeak].length + 1
if (nowSpeak < contentList.lastIndex) { if (nowSpeak < contentList.lastIndex) {
nowSpeak++ nowSpeak++

@ -1 +1,7 @@
## android服务 ## android服务
* AudioPlayService 音频播放服务
* CheckSourceService 书源检测服务
* DownloadService 缓存服务
* HttpReadAloudService 在线朗读服务
* TTSReadAloudService tts朗读服务
* WebService web服务

@ -6,12 +6,12 @@ import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener import android.speech.tts.UtteranceProgressListener
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -96,7 +96,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
textToSpeech = TextToSpeech(this, this) textToSpeech = TextToSpeech(this, this)
} }
} else { } else {
textToSpeech?.setSpeechRate((this.getPrefInt("ttsSpeechRate", 5) + 5) / 10f) textToSpeech?.setSpeechRate((AppConfig.ttsSpeechRate + 5) / 10f)
} }
} }
@ -152,7 +152,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
} }
} }
postEvent(Bus.TTS_START, readAloudNumber + 1) postEvent(EventBus.TTS_START, readAloudNumber + 1)
} }
override fun onDone(s: String) { override fun onDone(s: String) {
@ -169,7 +169,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) { if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) {
pageIndex++ pageIndex++
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
postEvent(Bus.TTS_START, readAloudNumber + start) postEvent(EventBus.TTS_START, readAloudNumber + start)
} }
} }
} }

@ -6,9 +6,9 @@ import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
@ -32,7 +32,7 @@ class WebService : BaseService() {
fun stop(context: Context) { fun stop(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, WebService::class.java) val intent = Intent(context, WebService::class.java)
intent.action = Action.stop intent.action = IntentAction.stop
context.startService(intent) context.startService(intent)
} }
} }
@ -56,12 +56,12 @@ class WebService : BaseService() {
if (webSocketServer?.isAlive == true) { if (webSocketServer?.isAlive == true) {
webSocketServer?.stop() webSocketServer?.stop()
} }
postEvent(Bus.WEB_SERVICE_STOP, true) postEvent(EventBus.WEB_SERVICE_STOP, true)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) { when (intent?.action) {
Action.stop -> stopSelf() IntentAction.stop -> stopSelf()
else -> upWebServer() else -> upWebServer()
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
@ -115,7 +115,7 @@ class WebService : BaseService() {
builder.addAction( builder.addAction(
R.drawable.ic_stop_black_24dp, R.drawable.ic_stop_black_24dp,
getString(R.string.cancel), getString(R.string.cancel),
IntentHelp.servicePendingIntent<WebService>(this, Action.stop) IntentHelp.servicePendingIntent<WebService>(this, IntentAction.stop)
) )
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build() val notification = builder.build()

@ -3,7 +3,7 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
@ -27,14 +27,14 @@ object AudioPlay {
fun play(context: Context) { fun play(context: Context) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.play intent.action = IntentAction.play
context.startService(intent) context.startService(intent)
} }
fun pause(context: Context) { fun pause(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.pause intent.action = IntentAction.pause
context.startService(intent) context.startService(intent)
} }
} }
@ -42,7 +42,7 @@ object AudioPlay {
fun resume(context: Context) { fun resume(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.resume intent.action = IntentAction.resume
context.startService(intent) context.startService(intent)
} }
} }
@ -50,7 +50,7 @@ object AudioPlay {
fun stop(context: Context) { fun stop(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.stop intent.action = IntentAction.stop
context.startService(intent) context.startService(intent)
} }
} }
@ -58,7 +58,7 @@ object AudioPlay {
fun adjustSpeed(context: Context, adjust: Float) { fun adjustSpeed(context: Context, adjust: Float) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.adjustSpeed intent.action = IntentAction.adjustSpeed
intent.putExtra("adjust", adjust) intent.putExtra("adjust", adjust)
context.startService(intent) context.startService(intent)
} }
@ -67,7 +67,7 @@ object AudioPlay {
fun adjustProgress(context: Context, position: Int) { fun adjustProgress(context: Context, position: Int) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.adjustProgress intent.action = IntentAction.adjustProgress
intent.putExtra("position", position) intent.putExtra("position", position)
context.startService(intent) context.startService(intent)
} }
@ -76,7 +76,7 @@ object AudioPlay {
fun prev(context: Context) { fun prev(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.prev intent.action = IntentAction.prev
context.startService(intent) context.startService(intent)
} }
} }
@ -84,7 +84,7 @@ object AudioPlay {
fun next(context: Context) { fun next(context: Context) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.next intent.action = IntentAction.next
context.startService(intent) context.startService(intent)
} }
} }

@ -0,0 +1,35 @@
package io.legado.app.service.help
import android.content.Context
import android.content.Intent
import io.legado.app.R
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.service.CheckSourceService
import org.jetbrains.anko.toast
object CheckSource {
fun start(context: Context, sources: LinkedHashSet<BookSource>) {
if (sources.isEmpty()) {
context.toast(R.string.non_select)
return
}
val selectedIds: ArrayList<String> = arrayListOf()
sources.map {
selectedIds.add(it.bookSourceUrl)
}
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.start
it.putExtra("selectIds", selectedIds)
context.startService(it)
}
}
fun stop(context: Context) {
Intent(context, CheckSourceService::class.java).let {
it.action = IntentAction.stop
context.startService(it)
}
}
}

@ -2,14 +2,14 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.service.DownloadService import io.legado.app.service.DownloadService
object Download { object Download {
fun start(context: Context, bookUrl: String, start: Int, end: Int) { fun start(context: Context, bookUrl: String, start: Int, end: Int) {
Intent(context, DownloadService::class.java).let { Intent(context, DownloadService::class.java).let {
it.action = Action.start it.action = IntentAction.start
it.putExtra("bookUrl", bookUrl) it.putExtra("bookUrl", bookUrl)
it.putExtra("start", start) it.putExtra("start", start)
it.putExtra("end", end) it.putExtra("end", end)

@ -3,7 +3,8 @@ package io.legado.app.service.help
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.Action import io.legado.app.constant.IntentAction
import io.legado.app.constant.PreferKey
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.HttpReadAloudService import io.legado.app.service.HttpReadAloudService
import io.legado.app.service.TTSReadAloudService import io.legado.app.service.TTSReadAloudService
@ -13,7 +14,7 @@ object ReadAloud {
var aloudClass: Class<*> = getReadAloudClass() var aloudClass: Class<*> = getReadAloudClass()
fun getReadAloudClass(): Class<*> { fun getReadAloudClass(): Class<*> {
return if (App.INSTANCE.getPrefBoolean("readAloudOnLine")) { return if (App.INSTANCE.getPrefBoolean(PreferKey.readAloudOnLine)) {
HttpReadAloudService::class.java HttpReadAloudService::class.java
} else { } else {
TTSReadAloudService::class.java TTSReadAloudService::class.java
@ -29,7 +30,7 @@ object ReadAloud {
play: Boolean = true play: Boolean = true
) { ) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.play intent.action = IntentAction.play
intent.putExtra("title", title) intent.putExtra("title", title)
intent.putExtra("subtitle", subtitle) intent.putExtra("subtitle", subtitle)
intent.putExtra("pageIndex", pageIndex) intent.putExtra("pageIndex", pageIndex)
@ -41,7 +42,7 @@ object ReadAloud {
fun pause(context: Context) { fun pause(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.pause intent.action = IntentAction.pause
context.startService(intent) context.startService(intent)
} }
} }
@ -49,7 +50,7 @@ object ReadAloud {
fun resume(context: Context) { fun resume(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.resume intent.action = IntentAction.resume
context.startService(intent) context.startService(intent)
} }
} }
@ -57,7 +58,7 @@ object ReadAloud {
fun stop(context: Context) { fun stop(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.stop intent.action = IntentAction.stop
context.startService(intent) context.startService(intent)
} }
} }
@ -65,7 +66,7 @@ object ReadAloud {
fun prevParagraph(context: Context) { fun prevParagraph(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.prevParagraph intent.action = IntentAction.prevParagraph
context.startService(intent) context.startService(intent)
} }
} }
@ -73,7 +74,7 @@ object ReadAloud {
fun nextParagraph(context: Context) { fun nextParagraph(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.nextParagraph intent.action = IntentAction.nextParagraph
context.startService(intent) context.startService(intent)
} }
} }
@ -81,7 +82,7 @@ object ReadAloud {
fun upTtsSpeechRate(context: Context) { fun upTtsSpeechRate(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.upTtsSpeechRate intent.action = IntentAction.upTtsSpeechRate
context.startService(intent) context.startService(intent)
} }
} }
@ -89,7 +90,7 @@ object ReadAloud {
fun setTimer(context: Context, minute: Int) { fun setTimer(context: Context, minute: Int) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)
intent.action = Action.setTimer intent.action = IntentAction.setTimer
intent.putExtra("minute", minute) intent.putExtra("minute", minute)
context.startService(intent) context.startService(intent)
} }

@ -39,6 +39,7 @@ object ReadBook {
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
durPageIndex = book.durChapterPos durPageIndex = book.durChapterPos
isLocalBook = book.origin == BookType.local isLocalBook = book.origin == BookType.local
webBook = null
App.db.bookSourceDao().getBookSource(book.origin)?.let { App.db.bookSourceDao().getBookSource(book.origin)?.let {
webBook = WebBook(it) webBook = WebBook(it)
} }
@ -197,6 +198,7 @@ object ReadBook {
private fun download(index: Int) { private fun download(index: Int) {
book?.let { book -> book?.let { book ->
if (book.isLocalBook()) return
if (addLoading(index)) { if (addLoading(index)) {
Coroutine.async { Coroutine.async {
App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter ->

@ -2,21 +2,22 @@
* about 关于界面 * about 关于界面
* audio 音频播放界面 * audio 音频播放界面
* book\arrange 书架整理界面
* book\info 书籍信息查看 * book\info 书籍信息查看
* book\read 书籍阅读界面 * book\read 书籍阅读界面
* book\search 搜索书籍界面 * book\search 搜索书籍界面
* book\source 搜索书源界面 * book\source 搜索书源界面
* changecover 封面换源界面 * changeCover 封面换源界面
* changesource 换源界面 * changeSource 换源界面
* chapterlist 目录界面 * chapterList 目录界面
* config 配置界面 * config 配置界面
* download 下载界面 * download 下载界面
* explore 发现界面 * explore 发现界面
* filechooser 文件选择界面 * fileChooser 文件选择界面
* importbook 书籍导入界面 * importBook 书籍导入界面
* main 主界面 * main 主界面
* qrcode 二维码扫描界面 * qrCode 二维码扫描界面
* replacerule 替换净化界面 * replaceRule 替换净化界面
* rss\article 订阅条目界面 * rss\article 订阅条目界面
* rss\read 订阅阅读界面 * rss\read 订阅阅读界面
* rss\source 订阅源界面 * rss\source 订阅源界面

@ -14,7 +14,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -193,12 +193,12 @@ class AudioPlayActivity :
} }
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<Boolean>(Bus.MEDIA_BUTTON) { observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
if (it) { if (it) {
playButton() playButton()
} }
} }
observeEventSticky<Int>(Bus.AUDIO_STATE) { observeEventSticky<Int>(EventBus.AUDIO_STATE) {
AudioPlay.status = it AudioPlay.status = it
if (it == Status.PLAY) { if (it == Status.PLAY) {
fab_play_stop.setImageResource(R.drawable.ic_pause_24dp) fab_play_stop.setImageResource(R.drawable.ic_pause_24dp)
@ -206,19 +206,19 @@ class AudioPlayActivity :
fab_play_stop.setImageResource(R.drawable.ic_play_24dp) fab_play_stop.setImageResource(R.drawable.ic_play_24dp)
} }
} }
observeEventSticky<String>(Bus.AUDIO_SUB_TITLE) { observeEventSticky<String>(EventBus.AUDIO_SUB_TITLE) {
tv_sub_title.text = it tv_sub_title.text = it
} }
observeEventSticky<Int>(Bus.AUDIO_SIZE) { observeEventSticky<Int>(EventBus.AUDIO_SIZE) {
player_progress.max = it player_progress.max = it
tv_all_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") tv_all_time.text = DateFormatUtils.format(it.toLong(), "mm:ss")
} }
observeEventSticky<Int>(Bus.AUDIO_PROGRESS) { observeEventSticky<Int>(EventBus.AUDIO_PROGRESS) {
AudioPlay.durPageIndex = it AudioPlay.durPageIndex = it
if (!adjustProgress) player_progress.progress = it if (!adjustProgress) player_progress.progress = it
tv_dur_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") tv_dur_time.text = DateFormatUtils.format(it.toLong(), "mm:ss")
} }
observeEventSticky<Float>(Bus.AUDIO_SPEED) { observeEventSticky<Float>(EventBus.AUDIO_SPEED) {
tv_speed.text = String.format("%.1fX", it) tv_speed.text = String.format("%.1fX", it)
tv_speed.visible() tv_speed.visible()
} }

@ -91,7 +91,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
AudioPlay.book?.let { AudioPlay.book?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -142,7 +142,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
execute { execute {
AudioPlay.book?.let { AudioPlay.book?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
}.onSuccess { }.onSuccess {
success?.invoke() success?.invoke()

@ -0,0 +1,117 @@
package io.legado.app.ui.book.arrange
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 io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_arrange_book.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activity_arrange_book),
ArrangeBookAdapter.CallBack {
override val viewModel: ArrangeBookViewModel
get() = getViewModel(ArrangeBookViewModel::class.java)
override val groupList: ArrayList<BookGroup> = arrayListOf()
private lateinit var adapter: ArrangeBookAdapter
private var groupLiveData: LiveData<List<BookGroup>>? = null
private var booksLiveData: LiveData<List<Book>>? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
initData()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.arrange_book, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
}
return super.onCompatOptionsItemSelected(item)
}
private fun initView() {
ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this)
adapter = ArrangeBookAdapter(this, this)
recycler_view.adapter = adapter
cb_selected_all.onClick {
adapter.selectAll(!adapter.isSelectAll())
}
btn_delete.onClick {
if (adapter.selectedBooks.isEmpty()) {
toast(R.string.non_select)
return@onClick
}
alert(titleResource = R.string.sure, messageResource = R.string.sure_del) {
okButton {
viewModel.deleteBook(*adapter.selectedBooks.toTypedArray())
}
}.show().applyTint()
}
btn_to_group.onClick {
if (adapter.selectedBooks.isEmpty()) {
toast(R.string.non_select)
return@onClick
}
selectGroup()
}
}
private fun initData() {
groupLiveData?.removeObservers(this)
groupLiveData = App.db.bookGroupDao().liveDataAll()
groupLiveData?.observe(this, Observer {
groupList.clear()
groupList.addAll(it)
adapter.notifyDataSetChanged()
})
booksLiveData?.removeObservers(this)
booksLiveData = App.db.bookDao().observeAll()
booksLiveData?.observe(this, Observer {
adapter.setItems(it)
upSelectCount()
})
}
override fun selectGroup() {
}
override fun upSelectCount() {
cb_selected_all.isChecked = adapter.isSelectAll()
//重置全选的文字
if (cb_selected_all.isChecked) {
cb_selected_all.setText(R.string.cancel)
} else {
cb_selected_all.setText(R.string.select_all)
}
tv_select_count.text =
getString(R.string.select_count, adapter.selectedBooks.size, adapter.getItems().size)
}
override fun deleteBook(book: Book) {
alert(titleResource = R.string.sure, messageResource = R.string.sure_del) {
okButton {
viewModel.deleteBook(book)
}
}.show().applyTint()
}
}

@ -0,0 +1,87 @@
package io.legado.app.ui.book.arrange
import android.content.Context
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import kotlinx.android.synthetic.main.item_arrange_book.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class ArrangeBookAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_arrange_book) {
val selectedBooks: HashSet<Book> = hashSetOf()
fun isSelectAll(): Boolean {
return if (selectedBooks.isEmpty()) {
false
} else {
selectedBooks.size >= itemCount
}
}
fun selectAll(selectAll: Boolean) {
if (selectAll) {
getItems().forEach {
selectedBooks.add(it)
}
notifyDataSetChanged()
callBack.upSelectCount()
} else {
selectedBooks.clear()
notifyDataSetChanged()
callBack.upSelectCount()
}
}
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {
tv_name.text = item.name
tv_author.text = context.getString(R.string.author_show, item.author)
tv_group.text = getGroupName(item.group)
checkbox.isChecked = selectedBooks.contains(item)
checkbox.onClick {
if (checkbox.isChecked) {
selectedBooks.add(item)
} else {
selectedBooks.remove(item)
}
callBack.upSelectCount()
}
onClick {
checkbox.isChecked = !checkbox.isChecked
if (checkbox.isChecked) {
selectedBooks.add(item)
} else {
selectedBooks.remove(item)
}
callBack.upSelectCount()
}
tv_delete.onClick {
callBack.deleteBook(item)
}
tv_group.onClick {
callBack.selectGroup()
}
}
}
private fun getGroupName(groupId: Int): String {
callBack.groupList.forEach {
if (it.groupId == groupId) {
return it.groupName
}
}
return context.getString(R.string.group)
}
interface CallBack {
val groupList: List<BookGroup>
fun upSelectCount()
fun deleteBook(book: Book)
fun selectGroup()
}
}

@ -0,0 +1,18 @@
package io.legado.app.ui.book.arrange
import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
class ArrangeBookViewModel(application: Application) : BaseViewModel(application) {
fun deleteBook(vararg book: Book) {
execute {
App.db.bookDao().delete()
}
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.main.bookshelf package io.legado.app.ui.book.group
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -25,6 +25,7 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.requestInputMethod import io.legado.app.utils.requestInputMethod
@ -77,7 +78,12 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
recycler_view.adapter = adapter recycler_view.adapter = adapter
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult = val diffResult =
DiffUtil.calculateDiff(GroupDiffCallBack(adapter.getItems(), it)) DiffUtil.calculateDiff(
GroupDiffCallBack(
adapter.getItems(),
it
)
)
adapter.setItems(it, false) adapter.setItems(it, false)
diffResult.dispatchUpdatesTo(adapter) diffResult.dispatchUpdatesTo(adapter)
}) })

@ -1,4 +1,4 @@
package io.legado.app.ui.book.info package io.legado.app.ui.book.group
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -44,7 +44,10 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
fun show(manager: FragmentManager) { fun show(manager: FragmentManager) {
val fragment = GroupSelectDialog() val fragment = GroupSelectDialog()
fragment.show(manager, tag) fragment.show(
manager,
tag
)
} }
} }

@ -21,9 +21,11 @@ import io.legado.app.help.BlurTransformation
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.changecover.ChangeCoverDialog
import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
@ -40,7 +42,8 @@ class BookInfoActivity :
VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, theme = Theme.Dark), VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, theme = Theme.Dark),
GroupSelectDialog.CallBack, GroupSelectDialog.CallBack,
ChapterListAdapter.CallBack, ChapterListAdapter.CallBack,
ChangeSourceDialog.CallBack { ChangeSourceDialog.CallBack,
ChangeCoverDialog.CallBack {
private val requestCodeChapterList = 568 private val requestCodeChapterList = 568
private val requestCodeSourceEdit = 562 private val requestCodeSourceEdit = 562
@ -104,29 +107,14 @@ class BookInfoActivity :
return super.onMenuOpened(featureId, menu) return super.onMenuOpened(featureId, menu)
} }
private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default)
.apply(bitmapTransform(BlurTransformation(this, 25)))
}
private fun showBook(book: Book) { private fun showBook(book: Book) {
showCover(book)
tv_name.text = book.name tv_name.text = book.name
tv_author.text = getString(R.string.author_show, book.author) tv_author.text = getString(R.string.author_show, book.author)
tv_origin.text = getString(R.string.origin_show, book.originName) tv_origin.text = getString(R.string.origin_show, book.originName)
tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle) tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle)
tv_toc.text = getString(R.string.toc_s, book.latestChapterTitle) tv_toc.text = getString(R.string.toc_s, book.latestChapterTitle)
tv_intro.text = book.getDisplayIntro() tv_intro.text = book.getDisplayIntro()
book.getDisplayCover()?.let {
ImageLoader.load(this, it)
.centerCrop()
.into(iv_cover)
ImageLoader.load(this, it)
.transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover())
.centerCrop()
.apply(bitmapTransform(BlurTransformation(this, 25)))
.into(bg_book) //模糊、渐变、缩小效果
}
val kinds = book.getKindList() val kinds = book.getKindList()
if (kinds.isEmpty()) { if (kinds.isEmpty()) {
ll_kind.gone() ll_kind.gone()
@ -159,6 +147,21 @@ class BookInfoActivity :
} }
} }
private fun showCover(book: Book) {
iv_cover.load(book.getDisplayCover(), book.name, book.author)
ImageLoader.load(this, book.getDisplayCover())
.transition(DrawableTransitionOptions.withCrossFade(1500))
.thumbnail(defaultCover())
.centerCrop()
.apply(bitmapTransform(BlurTransformation(this, 25)))
.into(bg_book) //模糊、渐变、缩小效果
}
private fun defaultCover(): RequestBuilder<Drawable> {
return ImageLoader.load(this, R.drawable.image_cover_default)
.apply(bitmapTransform(BlurTransformation(this, 25)))
}
private fun showChapter(chapterList: List<BookChapter>) { private fun showChapter(chapterList: List<BookChapter>) {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
if (it.durChapterIndex < chapterList.size) { if (it.durChapterIndex < chapterList.size) {
@ -184,6 +187,11 @@ class BookInfoActivity :
} }
private fun initOnClick() { private fun initOnClick() {
iv_cover.onClick {
viewModel.bookData.value?.let {
ChangeCoverDialog.show(supportFragmentManager, it.name, it.author)
}
}
tv_read.onClick { tv_read.onClick {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
readBook(it) readBook(it)
@ -276,6 +284,14 @@ class BookInfoActivity :
viewModel.changeTo(book) viewModel.changeTo(book)
} }
override fun coverChangeTo(coverUrl: String) {
viewModel.bookData.value?.let {
it.coverUrl = coverUrl
viewModel.saveBook()
showCover(it)
}
}
override fun openChapter(chapter: BookChapter) { override fun openChapter(chapter: BookChapter) {
if (chapter.index != viewModel.durChapterIndex) { if (chapter.index != viewModel.durChapterIndex) {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {

@ -11,6 +11,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) { class BookInfoViewModel(application: Application) : BaseViewModel(application) {
@ -55,8 +56,10 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
book: Book, book: Book,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) { ) {
if (book.isLocalBook()) return
execute { execute {
if (book.isLocalBook()) {
loadChapter(book, changeDruChapterIndex)
} else {
isLoadingData.postValue(true) isLoadingData.postValue(true)
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
@ -78,6 +81,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
}
private fun loadChapter( private fun loadChapter(
book: Book, book: Book,
@ -85,6 +89,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
) { ) {
execute { execute {
isLoadingData.postValue(true) isLoadingData.postValue(true)
if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
if (changeDruChapterIndex == null) {
chapterListData.postValue(it)
isLoadingData.postValue(false)
} else {
changeDruChapterIndex(it)
}
}
} else {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this) WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) { .onSuccess(IO) {
@ -115,12 +131,13 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
}
fun changeTo(book: Book) { fun changeTo(book: Book) {
execute { execute {
if (inBookshelf) { if (inBookshelf) {
bookData.value?.let { bookData.value?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
App.db.bookDao().insert(book) App.db.bookDao().insert(book)
} }
@ -185,7 +202,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun delBook(success: (() -> Unit)?) { fun delBook(success: (() -> Unit)?) {
execute { execute {
bookData.value?.let { bookData.value?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
inBookshelf = false inBookshelf = false
}.onSuccess { }.onSuccess {

@ -8,7 +8,6 @@ import androidx.lifecycle.Observer
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.ImageLoader
import io.legado.app.ui.changecover.ChangeCoverDialog import io.legado.app.ui.changecover.ChangeCoverDialog
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_book_info_edit.* import kotlinx.android.synthetic.main.activity_book_info_edit.*
@ -59,10 +58,8 @@ class BookInfoEditActivity :
} }
private fun upCover() { private fun upCover() {
viewModel.book?.getDisplayCover()?.let { viewModel.book.let {
ImageLoader.load(this, it) iv_cover.load(it?.getDisplayCover(), it?.name, it?.author)
.centerCrop()
.into(iv_cover)
} }
} }

@ -13,13 +13,15 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -67,6 +69,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
private val requestCodeChapterList = 568 private val requestCodeChapterList = 568
private val requestCodeEditSource = 111 private val requestCodeEditSource = 111
private val requestCodeReplace = 312 private val requestCodeReplace = 312
private var menu: Menu? = null
override val viewModel: ReadBookViewModel override val viewModel: ReadBookViewModel
get() = getViewModel(ReadBookViewModel::class.java) get() = getViewModel(ReadBookViewModel::class.java)
@ -85,7 +88,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
initView() initView()
upScreenTimeOut() upScreenTimeOut()
ReadBook.callBack = this ReadBook.callBack = this
ReadBook.titleDate.observe(this, Observer { title_bar.title = it }) ReadBook.titleDate.observe(this, Observer {
title_bar.title = it
upMenu()
})
viewModel.initData(intent) viewModel.initData(intent)
} }
@ -146,6 +152,33 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
} }
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
this.menu = menu
upMenu()
return super.onPrepareOptionsMenu(menu)
}
private fun upMenu() {
menu?.let { menu ->
ReadBook.book?.let { book ->
val onLine = !book.isLocalBook()
for (i in 0 until menu.size) {
val item = menu[i]
when (item.groupId) {
R.id.menu_group_on_line -> item.isVisible = onLine
R.id.menu_group_local -> item.isVisible = !onLine
R.id.menu_group_text -> item.isVisible = book.isTxt()
R.id.menu_group_login ->
item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
else -> if (item.itemId == R.id.menu_enable_replace) {
item.isChecked = book.useReplaceRule
}
}
}
}
}
}
/** /**
* 菜单 * 菜单
*/ */
@ -208,7 +241,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
ReadBook.durChapterIndex, ReadBook.durChapterIndex,
ReadBook.durPageIndex, ReadBook.durPageIndex,
textChapter!!.title, textChapter!!.title,
editContent) editContent
)
App.db.bookmarkDao().insert(bookmark) App.db.bookmarkDao().insert(bookmark)
} }
} }
@ -219,6 +253,13 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_copy_text -> { R.id.menu_copy_text -> {
} }
R.id.menu_update_toc -> ReadBook.book?.let {
viewModel.loadChapterList(it)
}
R.id.menu_enable_replace -> ReadBook.book?.let {
it.useReplaceRule = !it.useReplaceRule
menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.useReplaceRule
}
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
@ -375,7 +416,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
} }
override fun showMenu() { override fun showMenuBar() {
read_menu.runMenuIn() read_menu.runMenuIn()
} }
@ -457,12 +498,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (dialogId) { when (dialogId) {
TEXT_COLOR -> { TEXT_COLOR -> {
setTextColor(color) setTextColor(color)
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
BG_COLOR -> { BG_COLOR -> {
setBg(0, "#${color.hexString}") setBg(0, "#${color.hexString}")
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
} }
} }
@ -509,7 +550,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun observeLiveBus() { override fun observeLiveBus() {
super.observeLiveBus() super.observeLiveBus()
observeEvent<Int>(Bus.ALOUD_STATE) { observeEvent<Int>(EventBus.ALOUD_STATE) {
if (it == Status.STOP || it == Status.PAUSE) { if (it == Status.STOP || it == Status.PAUSE) {
ReadBook.curTextChapter?.let { textChapter -> ReadBook.curTextChapter?.let { textChapter ->
val page = textChapter.page(ReadBook.durPageIndex) val page = textChapter.page(ReadBook.durPageIndex)
@ -520,20 +561,20 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
} }
} }
observeEvent<String>(Bus.TIME_CHANGED) { page_view.upTime() } observeEvent<String>(EventBus.TIME_CHANGED) { page_view.upTime() }
observeEvent<Int>(Bus.BATTERY_CHANGED) { page_view.upBattery(it) } observeEvent<Int>(EventBus.BATTERY_CHANGED) { page_view.upBattery(it) }
observeEvent<BookChapter>(Bus.OPEN_CHAPTER) { observeEvent<BookChapter>(EventBus.OPEN_CHAPTER) {
viewModel.openChapter(it.index, ReadBook.durPageIndex) viewModel.openChapter(it.index, ReadBook.durPageIndex)
page_view.upContent() page_view.upContent()
} }
observeEvent<Boolean>(Bus.MEDIA_BUTTON) { observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
if (it) { if (it) {
onClickReadAloud() onClickReadAloud()
} else { } else {
ReadBook.readAloud(!BaseReadAloudService.pause) ReadBook.readAloud(!BaseReadAloudService.pause)
} }
} }
observeEvent<Boolean>(Bus.UP_CONFIG) { observeEvent<Boolean>(EventBus.UP_CONFIG) {
upSystemUiVisibility() upSystemUiVisibility()
content_view.upStyle() content_view.upStyle()
page_view.upBg() page_view.upBg()
@ -544,7 +585,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upContent() page_view.upContent()
} }
} }
observeEventSticky<Int>(Bus.TTS_START) { chapterStart -> observeEventSticky<Int>(EventBus.TTS_START) { chapterStart ->
launch(IO) { launch(IO) {
if (BaseReadAloudService.isPlay()) { if (BaseReadAloudService.isPlay()) {
ReadBook.curTextChapter?.let { ReadBook.curTextChapter?.let {
@ -557,7 +598,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
} }
} }
observeEvent<String>(Bus.REPLACE) { observeEvent<String>(EventBus.REPLACE) {
ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog") ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog")
} }
observeEvent<Boolean>(PreferKey.keepLight) { observeEvent<Boolean>(PreferKey.keepLight) {

@ -10,6 +10,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
@ -79,23 +80,36 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) { ) {
execute { execute {
if (book.isLocalBook()) {
loadChapterList(book, changeDruChapterIndex)
} else {
ReadBook.webBook?.getBookInfo(book, this) ReadBook.webBook?.getBookInfo(book, this)
?.onSuccess { ?.onSuccess {
loadChapterList(book, changeDruChapterIndex) loadChapterList(book, changeDruChapterIndex)
} }
} }
} }
}
private fun loadChapterList( fun loadChapterList(
book: Book, book: Book,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) { ) {
execute { execute {
if (book.isLocalBook()) {
AnalyzeTxtFile.analyze(context, book).let {
App.db.bookChapterDao().insert(*it.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = it.size
ReadBook.loadContent()
}
} else {
ReadBook.webBook?.getChapterList(book, this) ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList -> ?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) { if (!cList.isNullOrEmpty()) {
if (changeDruChapterIndex == null) { if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray()) App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size ReadBook.chapterSize = cList.size
ReadBook.loadContent() ReadBook.loadContent()
} else { } else {
@ -109,11 +123,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} ?: autoChangeSource() } ?: autoChangeSource()
} }
} }
}
fun changeTo(book1: Book) { fun changeTo(book1: Book) {
execute { execute {
ReadBook.book?.let { ReadBook.book?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
ReadBook.prevTextChapter = null ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null ReadBook.curTextChapter = null
@ -170,7 +185,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
execute { execute {
ReadBook.book?.let { ReadBook.book?.let {
App.db.bookDao().delete(it.bookUrl) App.db.bookDao().delete(it)
} }
}.onSuccess { }.onSuccess {
success?.invoke() success?.invoke()

@ -20,7 +20,7 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
@ -54,8 +54,8 @@ class BgTextConfigDialog : DialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent) it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0) it.decorView.setPadding(0, 5, 0, 0)
val attr = it.attributes val attr = it.attributes
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM attr.gravity = Gravity.BOTTOM
@ -154,7 +154,7 @@ class BgTextConfigDialog : DialogFragment() {
this.onClick { this.onClick {
ReadBookConfig.getConfig().setBg(1, item) ReadBookConfig.getConfig().setBg(1, item)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
} }
} }
@ -178,7 +178,7 @@ class BgTextConfigDialog : DialogFragment() {
file.writeBytes(it) file.writeBytes(it)
ReadBookConfig.getConfig().setBg(2, file.absolutePath) ReadBookConfig.getConfig().setBg(2, file.absolutePath)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
} }
} else { } else {
@ -192,7 +192,7 @@ class BgTextConfigDialog : DialogFragment() {
FileUtils.getPath(requireContext(), uri)?.let { path -> FileUtils.getPath(requireContext(), uri)?.let { path ->
ReadBookConfig.getConfig().setBg(2, path) ReadBookConfig.getConfig().setBg(2, path)
ReadBookConfig.upBg() ReadBookConfig.upBg()
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
} }
.request() .request()

@ -12,7 +12,7 @@ import androidx.fragment.app.DialogFragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
@ -29,8 +29,8 @@ class MoreConfigDialog : DialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent) it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0) it.decorView.setPadding(0, 5, 0, 0)
val attr = it.attributes val attr = it.attributes
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM attr.gravity = Gravity.BOTTOM
@ -92,9 +92,8 @@ class MoreConfigDialog : DialogFragment() {
key: String? key: String?
) { ) {
when (key) { when (key) {
PreferKey.hideStatusBar -> postEvent(Bus.UP_CONFIG, true) PreferKey.hideStatusBar -> postEvent(EventBus.UP_CONFIG, true)
PreferKey.hideNavigationBar -> postEvent(Bus.UP_CONFIG, true) PreferKey.hideNavigationBar -> postEvent(EventBus.UP_CONFIG, true)
PreferKey.clickAllNext -> postEvent(Bus.UP_CONFIG, true)
PreferKey.keepLight -> postEvent(PreferKey.keepLight, true) PreferKey.keepLight -> postEvent(PreferKey.keepLight, true)
} }
} }

@ -8,7 +8,7 @@ import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -26,8 +26,6 @@ class PaddingConfigDialog : DialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes val attr = it.attributes
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
it.attributes = attr it.attributes = attr
@ -63,35 +61,35 @@ class PaddingConfigDialog : DialogFragment() {
private fun initView() = with(ReadBookConfig.getConfig()) { private fun initView() = with(ReadBookConfig.getConfig()) {
iv_padding_top_add.onClick { iv_padding_top_add.onClick {
seek_padding_top.progressAdd(1) seek_padding_top.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_top_remove.onClick { iv_padding_top_remove.onClick {
seek_padding_top.progressAdd(-1) seek_padding_top.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_bottom_add.onClick { iv_padding_bottom_add.onClick {
seek_padding_bottom.progressAdd(1) seek_padding_bottom.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_bottom_remove.onClick { iv_padding_bottom_remove.onClick {
seek_padding_bottom.progressAdd(-1) seek_padding_bottom.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_left_add.onClick { iv_padding_left_add.onClick {
seek_padding_left.progressAdd(1) seek_padding_left.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_left_remove.onClick { iv_padding_left_remove.onClick {
seek_padding_left.progressAdd(-1) seek_padding_left.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_right_add.onClick { iv_padding_right_add.onClick {
seek_padding_right.progressAdd(1) seek_padding_right.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_padding_right_remove.onClick { iv_padding_right_remove.onClick {
seek_padding_right.progressAdd(-1) seek_padding_right.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
seek_padding_top.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seek_padding_top.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -103,7 +101,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
seek_padding_bottom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seek_padding_bottom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -115,7 +113,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
seek_padding_left.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seek_padding_left.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -127,7 +125,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
seek_padding_right.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seek_padding_right.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -139,7 +137,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
} }

@ -12,12 +12,13 @@ import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus 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.ATH
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
class ReadAloudConfigDialog : DialogFragment() { class ReadAloudConfigDialog : DialogFragment() {
@ -58,17 +59,19 @@ class ReadAloudConfigDialog : DialogFragment() {
} }
class ReadAloudPreferenceFragment : PreferenceFragmentCompat(), class ReadAloudPreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
Preference.OnPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_aloud) addPreferencesFromResource(R.xml.pref_config_aloud)
upPreferenceSummary(
findPreference<ListPreference>(PreferKey.ttsSpeechPer),
AppConfig.ttsSpeechPer
)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
ATH.applyEdgeEffectColor(listView) ATH.applyEdgeEffectColor(listView)
bindPreferenceSummaryToValue(findPreference("ttsSpeechPer"))
} }
override fun onResume() { override fun onResume() {
@ -86,42 +89,35 @@ class ReadAloudConfigDialog : DialogFragment() {
key: String? key: String?
) { ) {
when (key) { when (key) {
"readAloudByPage" -> { PreferKey.readAloudByPage -> {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
postEvent(Bus.MEDIA_BUTTON, false) postEvent(EventBus.MEDIA_BUTTON, false)
} }
} }
"readAloudOnLine" -> { PreferKey.readAloudOnLine -> {
if (BaseReadAloudService.isRun) {
ReadAloud.stop(requireContext()) ReadAloud.stop(requireContext())
ReadAloud.aloudClass = ReadAloud.getReadAloudClass() ReadAloud.aloudClass = ReadAloud.getReadAloudClass()
} }
PreferKey.ttsSpeechPer -> {
upPreferenceSummary(
findPreference<ListPreference>(PreferKey.ttsSpeechPer),
AppConfig.ttsSpeechPer
)
ReadAloud.upTtsSpeechRate(requireContext())
} }
"ttsSpeechPer" -> ReadAloud.upTtsSpeechRate(requireContext())
} }
} }
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { private fun upPreferenceSummary(preference: Preference?, value: String) {
val stringValue = newValue.toString() when (preference) {
is ListPreference -> {
if (preference is ListPreference) { val index = preference.findIndexOfValue(value)
val index = preference.findIndexOfValue(stringValue)
// Set the summary to reflect the new value. // Set the summary to reflect the new value.
preference.setSummary(if (index >= 0) preference.entries[index] else null) preference.summary = if (index >= 0) preference.entries[index] else null
} else {
// For all other preferences, set the summary to the value's
preference?.summary = stringValue
} }
return true else -> {
preference?.summary = value
} }
private fun bindPreferenceSummaryToValue(preference: Preference?) {
preference?.apply {
onPreferenceChangeListener = this@ReadAloudPreferenceFragment
onPreferenceChange(
this,
context.getPrefString(key)
)
} }
} }

@ -9,12 +9,15 @@ import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.utils.* import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.observeEvent
import io.legado.app.utils.putPrefBoolean
import kotlinx.android.synthetic.main.dialog_read_aloud.* import kotlinx.android.synthetic.main.dialog_read_aloud.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
@ -57,15 +60,15 @@ class ReadAloudDialog : DialogFragment() {
} }
private fun initData() { private fun initData() {
observeEvent<Int>(Bus.ALOUD_STATE) { upPlayState() } observeEvent<Int>(EventBus.ALOUD_STATE) { upPlayState() }
observeEvent<Int>(Bus.TTS_DS) { seek_timer.progress = it } observeEvent<Int>(EventBus.TTS_DS) { seek_timer.progress = it }
upPlayState() upPlayState()
seek_timer.progress = BaseReadAloudService.timeMinute seek_timer.progress = BaseReadAloudService.timeMinute
tv_timer.text = tv_timer.text =
requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute) requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute)
cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked
seek_tts_SpeechRate.progress = requireContext().getPrefInt("ttsSpeechRate", 5) seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate
} }
private fun initOnChange() { private fun initOnChange() {
@ -83,7 +86,7 @@ class ReadAloudDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
requireContext().putPrefInt("ttsSpeechRate", seek_tts_SpeechRate.progress) AppConfig.ttsSpeechRate = seek_tts_SpeechRate.progress
upTtsSpeechRate() upTtsSpeechRate()
} }
}) })
@ -101,7 +104,7 @@ class ReadAloudDialog : DialogFragment() {
} }
private fun initOnClick() { private fun initOnClick() {
iv_menu.onClick { callBack?.showMenu(); dismiss() } iv_menu.onClick { callBack?.showMenuBar(); dismiss() }
iv_other_config.onClick { iv_other_config.onClick {
ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog")
} }
@ -135,7 +138,7 @@ class ReadAloudDialog : DialogFragment() {
} }
interface CallBack { interface CallBack {
fun showMenu() fun showMenuBar()
fun openChapterList() fun openChapterList()
fun onClickReadAloud() fun onClickReadAloud()
fun finish() fun finish()

@ -10,7 +10,7 @@ import android.widget.SeekBar
import androidx.core.view.get import androidx.core.view.get
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
@ -37,8 +37,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
it.windowManager?.defaultDisplay?.getMetrics(dm) it.windowManager?.defaultDisplay?.getMetrics(dm)
} }
dialog?.window?.let { dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent) it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0) it.decorView.setPadding(0, 5, 0, 0)
val attr = it.attributes val attr = it.attributes
attr.dimAmount = 0.0f attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM attr.gravity = Gravity.BOTTOM
@ -83,7 +83,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
textBold = !textBold textBold = !textBold
tv_text_bold.isSelected = textBold tv_text_bold.isSelected = textBold
} }
postEvent(Bus.UP_CONFIG, false) postEvent(EventBus.UP_CONFIG, false)
} }
tv_text_font.onClick { tv_text_font.onClick {
FontSelectDialog().show(childFragmentManager, "fontSelectDialog") FontSelectDialog().show(childFragmentManager, "fontSelectDialog")
@ -94,7 +94,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
items = resources.getStringArray(R.array.indent).toList() items = resources.getStringArray(R.array.indent).toList()
) { _, index -> ) { _, index ->
putPrefInt("textIndent", index) putPrefInt("textIndent", index)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }
tv_padding.onClick { tv_padding.onClick {
@ -112,16 +112,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
iv_text_size_add.onClick { iv_text_size_add.onClick {
seek_text_size.progressAdd(1) seek_text_size.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_text_size_remove.onClick { iv_text_size_remove.onClick {
seek_text_size.progressAdd(-1) seek_text_size.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
seek_text_letter_spacing.setOnSeekBarChangeListener(object : seek_text_letter_spacing.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener { SeekBar.OnSeekBarChangeListener {
@ -134,16 +134,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
iv_text_letter_spacing_add.onClick { iv_text_letter_spacing_add.onClick {
seek_text_letter_spacing.progressAdd(1) seek_text_letter_spacing.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_text_letter_spacing_remove.onClick { iv_text_letter_spacing_remove.onClick {
seek_text_letter_spacing.progressAdd(-1) seek_text_letter_spacing.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
seek_line_size.setOnSeekBarChangeListener(object : seek_line_size.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener { SeekBar.OnSeekBarChangeListener {
@ -156,16 +156,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
}) })
iv_line_size_add.onClick { iv_line_size_add.onClick {
seek_line_size.progressAdd(1) seek_line_size.progressAdd(1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
iv_line_size_remove.onClick { iv_line_size_remove.onClick {
seek_line_size.progressAdd(-1) seek_line_size.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
rg_page_anim.onCheckedChange { _, checkedId -> rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) { for (i in 0 until rg_page_anim.childCount) {
@ -197,7 +197,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
ReadBookConfig.upBg() ReadBookConfig.upBg()
upStyle() upStyle()
upBg() upBg()
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }
@ -266,6 +266,6 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun selectFile(path: String) { override fun selectFile(path: String) {
requireContext().putPrefString(PreferKey.readBookFont, path) requireContext().putPrefString(PreferKey.readBookFont, path)
postEvent(Bus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
} }

@ -6,7 +6,7 @@ import android.view.MenuItem
import android.widget.TextView import android.widget.TextView
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback { class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback {
@ -15,7 +15,7 @@ class ContentSelectActionCallback(private val textView: TextView) : ActionMode.C
when (item?.itemId) { when (item?.itemId) {
R.id.menu_replace -> { R.id.menu_replace -> {
val text = textView.text.substring(textView.selectionStart, textView.selectionEnd) val text = textView.text.substring(textView.selectionStart, textView.selectionEnd)
postEvent(Bus.REPLACE, text) postEvent(EventBus.REPLACE, text)
mode?.finish() mode?.finish()
return true return true
} }

@ -9,10 +9,9 @@ import android.widget.Scroller
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.legado.app.constant.PreferKey import io.legado.app.help.AppConfig
import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
import io.legado.app.utils.snackbar import io.legado.app.utils.snackbar
import kotlin.math.abs import kotlin.math.abs
@ -256,7 +255,8 @@ abstract class PageDelegate(protected val pageView: PageView) {
setTouchPoint(x, y) setTouchPoint(x, y)
} else { } else {
bitmap = if (x > viewWidth / 2 || bitmap = if (x > viewWidth / 2 ||
pageView.context.getPrefBoolean(PreferKey.clickAllNext, false)) { AppConfig.clickAllNext
) {
//设置动画方向 //设置动画方向
if (!hasNext()) { if (!hasNext()) {
return true return true

@ -1,6 +1,5 @@
package io.legado.app.ui.book.search package io.legado.app.ui.book.search
import android.content.Context
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
@ -15,8 +14,10 @@ import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
class HistoryKeyAdapter(context: Context, val callBack: CallBack) : class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) :
SimpleRecyclerAdapter<SearchKeyword>(context, R.layout.item_text) { SimpleRecyclerAdapter<SearchKeyword>(activity, R.layout.item_text) {
private val explosionField = ExplosionField.attach2Window(activity)
override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
@ -26,7 +27,7 @@ class HistoryKeyAdapter(context: Context, val callBack: CallBack) :
} }
onLongClick { onLongClick {
it?.let { it?.let {
ExplosionField(context).explode(it, true) explosionField.explode(it, true)
} }
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
App.db.searchKeywordDao().delete(item) App.db.searchKeywordDao().delete(item)

@ -67,13 +67,7 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
} }
} }
} }
searchBook.coverUrl.let { iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author)
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
onClick { onClick {
callBack.showBookInfo(searchBook.name, searchBook.author) callBack.showBookInfo(searchBook.name, searchBook.author)
} }

@ -30,7 +30,7 @@ import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.lib.theme.view.ATEAutoCompleteTextView import io.legado.app.lib.theme.view.ATEAutoCompleteTextView
import io.legado.app.service.CheckSourceService import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeActivity
@ -40,7 +40,6 @@ import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.view_search.* import kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.startService
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -81,21 +80,18 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
when (item.itemId) { when (item.itemId) {
R.id.menu_add_book_source -> startActivity<BookSourceEditActivity>() R.id.menu_add_book_source -> startActivity<BookSourceEditActivity>()
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode) R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_group_manage -> GroupManageDialog().show( R.id.menu_group_manage ->
supportFragmentManager, GroupManageDialog().show(supportFragmentManager, "groupManage")
"groupManage"
)
R.id.menu_import_source_local -> selectFileSys() R.id.menu_import_source_local -> selectFileSys()
R.id.menu_select_all -> adapter.selectAll() R.id.menu_select_all -> adapter.selectAll()
R.id.menu_revert_selection -> adapter.revertSelection() R.id.menu_revert_selection -> adapter.revertSelection()
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds()) R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds()) R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection())
R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelectionIds()) R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection())
R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelectionIds()) R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection())
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds()) R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelectionIds()) R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelection())
R.id.menu_check_source -> R.id.menu_check_source -> CheckSource.start(this, adapter.getSelection())
startService<CheckSourceService>(Pair("data", adapter.getSelectionIds()))
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_onLine -> showImportDialog()
} }
if (item.groupId == R.id.source_group) { if (item.groupId == R.id.source_group) {

@ -1,8 +1,10 @@
package io.legado.app.ui.book.source.manage package io.legado.app.ui.book.source.manage
import android.content.Context import android.content.Context
import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.os.bundleOf
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
@ -16,31 +18,31 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<BookSource>(context, R.layout.item_book_source), SimpleRecyclerAdapter<BookSource>(context, R.layout.item_book_source),
OnItemTouchCallbackListener { OnItemTouchCallbackListener {
private val selectedIds = linkedSetOf<String>() private val selected = linkedSetOf<BookSource>()
fun selectAll() { fun selectAll() {
getItems().forEach { getItems().forEach {
selectedIds.add(it.bookSourceUrl) selected.add(it)
} }
notifyItemRangeChanged(0, itemCount, 1) notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
} }
fun revertSelection() { fun revertSelection() {
getItems().forEach { getItems().forEach {
if (selectedIds.contains(it.bookSourceUrl)) { if (selected.contains(it)) {
selectedIds.remove(it.bookSourceUrl) selected.remove(it)
} else { } else {
selectedIds.add(it.bookSourceUrl) selected.add(it)
} }
} }
notifyItemRangeChanged(0, itemCount, 1) notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
} }
fun getSelectionIds(): LinkedHashSet<String> { fun getSelection(): LinkedHashSet<BookSource> {
val selection = linkedSetOf<String>() val selection = linkedSetOf<BookSource>()
getItems().map { getItems().map {
if (selectedIds.contains(it.bookSourceUrl)) { if (selected.contains(it)) {
selection.add(it.bookSourceUrl) selection.add(it)
} }
} }
return selection return selection
@ -68,7 +70,8 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
if (payloads.isEmpty()) { val payload = payloads.getOrNull(0) as? Bundle
if (payload == null) {
this.setBackgroundColor(context.backgroundColor) this.setBackgroundColor(context.backgroundColor)
if (item.bookSourceGroup.isNullOrEmpty()) { if (item.bookSourceGroup.isNullOrEmpty()) {
cb_book_source.text = item.bookSourceName cb_book_source.text = item.bookSourceName
@ -81,12 +84,12 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
item.enabled = swt_enabled.isChecked item.enabled = swt_enabled.isChecked
callBack.update(item) callBack.update(item)
} }
cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl) cb_book_source.isChecked = selected.contains(item)
cb_book_source.setOnClickListener { cb_book_source.setOnClickListener {
if (cb_book_source.isChecked) { if (cb_book_source.isChecked) {
selectedIds.add(item.bookSourceUrl) selected.add(item)
} else { } else {
selectedIds.remove(item.bookSourceUrl) selected.remove(item)
} }
} }
iv_edit.onClick { callBack.edit(item) } iv_edit.onClick { callBack.edit(item) }
@ -104,9 +107,17 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
popupMenu.show() popupMenu.show()
} }
} else { } else {
when (payloads[0]) { payload.keySet().map {
1 -> cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl) when (it) {
2 -> swt_enabled.isChecked = item.enabled "select" -> cb_book_source.isChecked = selected.contains(item)
"name", "group" -> if (item.bookSourceGroup.isNullOrEmpty()) {
cb_book_source.text = item.bookSourceName
} else {
cb_book_source.text =
String.format("%s (%s)", item.bookSourceName, item.bookSourceGroup)
}
"enabled" -> swt_enabled.isChecked = payload.getBoolean(it)
}
} }
} }
} }

@ -43,56 +43,54 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun enableSelection(ids: LinkedHashSet<String>) { fun enableSelection(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.forEach { sources.forEach {
App.db.bookSourceDao().enableSection(it) it.enabled = true
} }
App.db.bookSourceDao().update(*sources.toTypedArray())
} }
} }
fun disableSelection(ids: LinkedHashSet<String>) { fun disableSelection(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.forEach { sources.forEach {
App.db.bookSourceDao().disableSection(it) it.enabled = false
} }
App.db.bookSourceDao().update(*sources.toTypedArray())
} }
} }
fun enableSelectExplore(ids: LinkedHashSet<String>) { fun enableSelectExplore(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.forEach { sources.forEach {
App.db.bookSourceDao().enableSectionExplore(it) it.enabledExplore = true
} }
App.db.bookSourceDao().update(*sources.toTypedArray())
} }
} }
fun disableSelectExplore(ids: LinkedHashSet<String>) { fun disableSelectExplore(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.forEach { sources.forEach {
App.db.bookSourceDao().disableSectionExplore(it) it.enabledExplore = false
} }
App.db.bookSourceDao().update(*sources.toTypedArray())
} }
} }
fun delSelection(ids: LinkedHashSet<String>) { fun delSelection(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.forEach { App.db.bookSourceDao().delete(*sources.toTypedArray())
App.db.bookSourceDao().delSection(it)
}
} }
} }
fun exportSelection(ids: LinkedHashSet<String>) { fun exportSelection(sources: LinkedHashSet<BookSource>) {
execute { execute {
ids.map { val json = GSON.toJson(sources)
App.db.bookSourceDao().getBookSource(it)
}.let {
val json = GSON.toJson(it)
val file = val file =
FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportBookSource.json") FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportBookSource.json")
file.writeText(json) file.writeText(json)
}
}.onSuccess { }.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}") context.toast("成功导出至\n${Backup.exportPath}")
}.onError { }.onError {

@ -1,5 +1,6 @@
package io.legado.app.ui.book.source.manage package io.legado.app.ui.book.source.manage
import android.os.Bundle
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
@ -25,19 +26,31 @@ class DiffCallBack(
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
return oldItem.bookSourceName == newItem.bookSourceName if (oldItem.bookSourceName != newItem.bookSourceName)
&& oldItem.bookSourceGroup == newItem.bookSourceGroup return false
&& oldItem.enabled == newItem.enabled if (oldItem.bookSourceGroup != newItem.bookSourceGroup)
return false
if (oldItem.enabled != newItem.enabled)
return false
return true
} }
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
return when { val payload = Bundle()
oldItem.bookSourceName == newItem.bookSourceName if (oldItem.bookSourceName != newItem.bookSourceName) {
&& oldItem.bookSourceGroup == newItem.bookSourceGroup payload.putString("name", newItem.bookSourceName)
&& oldItem.enabled != newItem.enabled -> 2
else -> null
} }
if (oldItem.bookSourceGroup != newItem.bookSourceGroup) {
payload.putString("group", newItem.bookSourceGroup)
}
if (oldItem.enabled != newItem.enabled) {
payload.putBoolean("enabled", newItem.enabled)
}
if (payload.isEmpty) {
return null
}
return payload
} }
} }

@ -7,13 +7,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.dialog_change_source.* import kotlinx.android.synthetic.main.dialog_change_source.*
class ChangeCoverDialog : DialogFragment() { class ChangeCoverDialog : DialogFragment(),
ChangeCoverViewModel.CallBack,
CoverAdapter.CallBack {
companion object { companion object {
const val tag = "changeCoverDialog" const val tag = "changeCoverDialog"
@ -32,7 +35,7 @@ class ChangeCoverDialog : DialogFragment() {
private var callBack: CallBack? = null private var callBack: CallBack? = null
private lateinit var viewModel: ChangeCoverViewModel private lateinit var viewModel: ChangeCoverViewModel
private lateinit var adapter: CoverAdapter override lateinit var adapter: CoverAdapter
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -46,13 +49,17 @@ class ChangeCoverDialog : DialogFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
callBack = activity as? CallBack
viewModel = getViewModel(ChangeCoverViewModel::class.java) viewModel = getViewModel(ChangeCoverViewModel::class.java)
viewModel.callBack = this
return inflater.inflate(R.layout.dialog_change_source, container) return inflater.inflate(R.layout.dialog_change_source, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
callBack = activity as? CallBack viewModel.searchStateData.observe(this, Observer {
refresh_progress_bar.isAutoLoading = it
})
tool_bar.setTitle(R.string.change_cover_source) tool_bar.setTitle(R.string.change_cover_source)
arguments?.let { bundle -> arguments?.let { bundle ->
bundle.getString("name")?.let { bundle.getString("name")?.let {
@ -63,8 +70,14 @@ class ChangeCoverDialog : DialogFragment() {
} }
} }
recycler_view.layoutManager = GridLayoutManager(requireContext(), 3) recycler_view.layoutManager = GridLayoutManager(requireContext(), 3)
adapter = CoverAdapter(requireContext()) adapter = CoverAdapter(requireContext(), this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
viewModel.initData()
}
override fun changeTo(coverUrl: String) {
callBack?.coverChangeTo(coverUrl)
dismiss()
} }
interface CallBack { interface CallBack {

@ -1,12 +1,77 @@
package io.legado.app.ui.changecover package io.legado.app.ui.changecover
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
var callBack: CallBack? = null
var name: String = "" var name: String = ""
var author: String = "" var author: String = ""
private var task: Coroutine<*>? = null
val searchStateData = MutableLiveData<Boolean>()
fun initData() {
execute {
App.db.searchBookDao().getEnableHasCover(name, author)
}.onSuccess {
it?.let {
callBack?.adapter?.setItems(it)
}
}.onFinally {
search()
}
}
fun search() {
task = execute {
searchStateData.postValue(true)
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 != null && it.isNotEmpty()) {
val searchBook = it[0]
if (searchBook.name == name && searchBook.author == author
&& !searchBook.coverUrl.isNullOrEmpty()
) {
App.db.searchBookDao().insert(searchBook)
callBack?.adapter?.let { adapter ->
if (!adapter.getItems().contains(searchBook)) {
withContext(Dispatchers.Main) {
adapter.addItem(searchBook)
}
}
}
}
}
}
}
}
task?.invokeOnCompletion {
searchStateData.postValue(false)
}
}
override fun onCleared() {
super.onCleared()
searchPool.close()
}
interface CallBack {
var adapter: CoverAdapter
}
} }

@ -5,21 +5,23 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.help.ImageLoader
import kotlinx.android.synthetic.main.item_cover.view.* import kotlinx.android.synthetic.main.item_cover.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class CoverAdapter(context: Context) : class CoverAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<SearchBook>(context, R.layout.item_cover) { SimpleRecyclerAdapter<SearchBook>(context, R.layout.item_cover) {
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
item.coverUrl?.let { iv_cover.load(item.coverUrl, item.name, item.author)
ImageLoader.load(context, it)
.centerCrop()
.into(iv_cover)
}
tv_source.text = item.originName tv_source.text = item.originName
onClick {
callBack.changeTo(item.coverUrl!!)
}
} }
} }
interface CallBack {
fun changeTo(coverUrl: String)
}
} }

@ -40,7 +40,7 @@ class ChangeSourceDialog : DialogFragment(),
private var callBack: CallBack? = null private var callBack: CallBack? = null
private lateinit var viewModel: ChangeSourceViewModel private lateinit var viewModel: ChangeSourceViewModel
private lateinit var changeSourceAdapter: ChangeSourceAdapter override lateinit var changeSourceAdapter: ChangeSourceAdapter
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -54,13 +54,14 @@ class ChangeSourceDialog : DialogFragment(),
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
callBack = activity as? CallBack
viewModel = getViewModel(ChangeSourceViewModel::class.java) viewModel = getViewModel(ChangeSourceViewModel::class.java)
viewModel.callBack = this
return inflater.inflate(R.layout.dialog_change_source, container) return inflater.inflate(R.layout.dialog_change_source, container)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
callBack = activity as? CallBack
viewModel.searchStateData.observe(viewLifecycleOwner, Observer { viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it refresh_progress_bar.isAutoLoading = it
}) })
@ -92,7 +93,6 @@ class ChangeSourceDialog : DialogFragment(),
DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
) )
recycler_view.adapter = changeSourceAdapter recycler_view.adapter = changeSourceAdapter
viewModel.callBack = this
} }
private fun initSearchView() { private fun initSearchView() {
@ -139,10 +139,6 @@ class ChangeSourceDialog : DialogFragment(),
override val bookUrl: String? override val bookUrl: String?
get() = callBack?.oldBook?.bookUrl get() = callBack?.oldBook?.bookUrl
override fun adapter(): ChangeSourceAdapter {
return changeSourceAdapter
}
interface CallBack { interface CallBack {
val oldBook: Book? val oldBook: Book?
fun changeTo(book: Book) fun changeTo(book: Book)

@ -40,9 +40,8 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
private fun upAdapter() { private fun upAdapter() {
execute { execute {
callBack?.adapter()?.let { callBack?.changeSourceAdapter?.let {
val books = searchBooks.toList() val books = searchBooks.toList()
books.sorted()
val diffResult = DiffUtil.calculateDiff(DiffCallBack(it.getItems(), books)) val diffResult = DiffUtil.calculateDiff(DiffCallBack(it.getItems(), books))
withContext(Main) { withContext(Main) {
synchronized(this) { synchronized(this) {
@ -130,12 +129,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
interface CallBack {
fun adapter(): ChangeSourceAdapter
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchPool.close() searchPool.close()
} }
interface CallBack {
var changeSourceAdapter: ChangeSourceAdapter
}
} }

@ -74,8 +74,6 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
} }
override fun delBookmark(bookmark: Bookmark) { override fun delBookmark(bookmark: Bookmark) {
bookmark?.let { App.db.bookmarkDao().delByBookmark(bookmark.bookUrl, bookmark.chapterName)
App.db.bookmarkDao().delByBookmark(it.bookUrl, it.chapterName)
}
} }
} }

@ -47,26 +47,23 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
fun bindPreferenceSummaryToValue(preference: Preference?) { fun bindPreferenceSummaryToValue(preference: Preference?) {
preference?.apply { preference?.apply {
onPreferenceChangeListener = this@BackupConfigFragment onPreferenceChangeListener = this@BackupConfigFragment
onPreferenceChange( onPreferenceChange(this, context.getPrefString(key))
this,
context.getPrefString(key)
)
} }
} }
addPreferencesFromResource(R.xml.pref_config_backup) addPreferencesFromResource(R.xml.pref_config_backup)
findPreference<EditTextPreference>("web_dav_url")?.let { findPreference<EditTextPreference>(PreferKey.webDavUrl)?.let {
it.setOnBindEditTextListener { editText -> it.setOnBindEditTextListener { editText ->
ATH.setTint(editText, requireContext().accentColor) ATH.setTint(editText, requireContext().accentColor)
} }
bindPreferenceSummaryToValue(it) bindPreferenceSummaryToValue(it)
} }
findPreference<EditTextPreference>("web_dav_account")?.let { findPreference<EditTextPreference>(PreferKey.webDavAccount)?.let {
it.setOnBindEditTextListener { editText -> it.setOnBindEditTextListener { editText ->
ATH.setTint(editText, requireContext().accentColor) ATH.setTint(editText, requireContext().accentColor)
} }
bindPreferenceSummaryToValue(it) bindPreferenceSummaryToValue(it)
} }
findPreference<EditTextPreference>("web_dav_password")?.let { findPreference<EditTextPreference>(PreferKey.webDavPassword)?.let {
it.setOnBindEditTextListener { editText -> it.setOnBindEditTextListener { editText ->
ATH.setTint(editText, requireContext().accentColor) ATH.setTint(editText, requireContext().accentColor)
editText.inputType = editText.inputType =
@ -88,30 +85,35 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
} }
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
when { when (preference?.key) {
preference?.key == "web_dav_password" -> if (newValue == null) { PreferKey.webDavUrl ->
preference.summary = getString(R.string.web_dav_pw_s) if (newValue == null) {
} else {
preference.summary = "*".repeat(newValue.toString().length)
}
preference?.key == "web_dav_url" -> if (newValue == null) {
preference.summary = getString(R.string.web_dav_url_s) preference.summary = getString(R.string.web_dav_url_s)
} else { } else {
preference.summary = newValue.toString() preference.summary = newValue.toString()
} }
preference?.key == "web_dav_account" -> if (newValue == null) { PreferKey.webDavAccount ->
if (newValue == null) {
preference.summary = getString(R.string.web_dav_account_s) preference.summary = getString(R.string.web_dav_account_s)
} else { } else {
preference.summary = newValue.toString() preference.summary = newValue.toString()
} }
preference is ListPreference -> { PreferKey.webDavPassword ->
if (newValue == null) {
preference.summary = getString(R.string.web_dav_pw_s)
} else {
preference.summary = "*".repeat(newValue.toString().length)
}
else ->
if (preference is ListPreference) {
val index = preference.findIndexOfValue(newValue?.toString()) val index = preference.findIndexOfValue(newValue?.toString())
// Set the summary to reflect the new value. // Set the summary to reflect the new value.
preference.setSummary(if (index >= 0) preference.entries[index] else null) preference.setSummary(if (index >= 0) preference.entries[index] else null)
} else {
preference?.summary = newValue?.toString()
} }
else -> preference?.summary = newValue?.toString()
} }
return true return false
} }
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
@ -132,6 +134,7 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
if (doc?.canWrite() == true) { if (doc?.canWrite() == true) {
launch { launch {
Backup.backup(requireContext(), uri) Backup.backup(requireContext(), uri)
toast(R.string.backup_success)
} }
} else { } else {
selectBackupFolder() selectBackupFolder()
@ -153,6 +156,7 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
.onGranted { .onGranted {
launch { launch {
Backup.backup(requireContext(), null) Backup.backup(requireContext(), null)
toast(R.string.backup_success)
} }
} }
.request() .request()
@ -161,7 +165,9 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
fun restore() { fun restore() {
launch { launch {
if (!WebDavHelp.showRestoreDialog(requireContext())) { if (!WebDavHelp.showRestoreDialog(requireContext()) {
toast(R.string.restore_success)
}) {
val backupPath = getPrefString(PreferKey.backupPath) val backupPath = getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) { if (backupPath?.isNotEmpty() == true) {
val uri = Uri.parse(backupPath) val uri = Uri.parse(backupPath)
@ -308,8 +314,10 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
putPrefString(PreferKey.backupPath, uri.toString()) putPrefString(PreferKey.backupPath, uri.toString())
findPreference<Preference>(PreferKey.backupPath)?.summary = uri.toString()
launch { launch {
Backup.backup(requireContext(), uri) Backup.backup(requireContext(), uri)
toast(R.string.backup_success)
} }
} }
} }
@ -320,6 +328,7 @@ class BackupConfigFragment : PreferenceFragmentCompat(),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
putPrefString(PreferKey.backupPath, uri.toString()) putPrefString(PreferKey.backupPath, uri.toString())
findPreference<Preference>(PreferKey.backupPath)?.summary = uri.toString()
launch { launch {
Restore.restore(requireContext(), uri) Restore.restore(requireContext(), uri)
toast(R.string.restore_success) toast(R.string.restore_success)

@ -12,7 +12,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
@ -27,7 +27,6 @@ import io.legado.app.utils.*
class OtherConfigFragment : PreferenceFragmentCompat(), class OtherConfigFragment : PreferenceFragmentCompat(),
FileChooserDialog.CallBack, FileChooserDialog.CallBack,
Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeDownloadPath = 25324 private val requestCodeDownloadPath = 25324
@ -40,8 +39,8 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean("process_text", isProcessTextEnabled()) putPrefBoolean("process_text", isProcessTextEnabled())
addPreferencesFromResource(R.xml.pref_config_other) addPreferencesFromResource(R.xml.pref_config_other)
bindPreferenceSummaryToValue(findPreference(PreferKey.downloadPath)) upPreferenceSummary(findPreference(PreferKey.downloadPath), BookHelp.downloadPath)
bindPreferenceSummaryToValue(findPreference(PreferKey.threadCount)) upPreferenceSummary(findPreference(PreferKey.threadCount), AppConfig.threadCount.toString())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -84,48 +83,26 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
when (key) { when (key) {
PreferKey.downloadPath -> { PreferKey.downloadPath -> {
BookHelp.upDownloadPath() BookHelp.upDownloadPath()
findPreference<Preference>(key)?.summary = getPreferenceString(key).toString() findPreference<Preference>(key)?.summary = BookHelp.downloadPath
} }
PreferKey.recordLog -> LogUtils.upLevel() PreferKey.recordLog -> LogUtils.upLevel()
PreferKey.processText -> sharedPreferences?.let { PreferKey.processText -> sharedPreferences?.let {
setProcessTextEnable(it.getBoolean("process_text", true)) setProcessTextEnable(it.getBoolean("process_text", true))
} }
PreferKey.showRss -> postEvent(Bus.SHOW_RSS, "unused") PreferKey.showRss -> postEvent(EventBus.SHOW_RSS, "unused")
} }
} }
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { private fun upPreferenceSummary(preference: Preference?, value: String) {
val stringValue = newValue.toString() when (preference) {
when { is ListPreference -> {
preference is ListPreference -> { val index = preference.findIndexOfValue(value)
val index = preference.findIndexOfValue(stringValue)
// Set the summary to reflect the new value. // Set the summary to reflect the new value.
preference.setSummary(if (index >= 0) preference.entries[index] else null) preference.summary = if (index >= 0) preference.entries[index] else null
} }
preference?.key == PreferKey.threadCount -> preference.summary = else -> {
getString(R.string.threads_num, stringValue) preference?.summary = value
else -> preference?.summary = stringValue
} }
return true
}
private fun bindPreferenceSummaryToValue(preference: Preference?) {
preference?.let {
preference.onPreferenceChangeListener = this
onPreferenceChange(
preference,
getPreferenceString(preference.key)
)
}
}
private fun getPreferenceString(key: String): Any {
return when (key) {
PreferKey.downloadPath -> getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
PreferKey.threadCount -> AppConfig.threadCount
else -> getPrefString(key) ?: ""
} }
} }
@ -166,7 +143,7 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
childFragmentManager, childFragmentManager,
requestCodeDownloadPath, requestCodeDownloadPath,
mode = FileChooserDialog.DIRECTORY, mode = FileChooserDialog.DIRECTORY,
initPath = getPreferenceString(PreferKey.downloadPath).toString() initPath = BookHelp.downloadPath
) )
} }
.request() .request()
@ -188,6 +165,8 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
putPrefString(PreferKey.downloadPath, uri.toString()) putPrefString(PreferKey.downloadPath, uri.toString())
findPreference<Preference>(PreferKey.downloadPath)?.summary = uri.toString()
BookHelp.upDownloadPath()
} }
} }
} }

@ -9,7 +9,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
@ -104,7 +104,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
.setTitle("切换默认主题") .setTitle("切换默认主题")
.setItems(items){ .setItems(items){
_,which -> _,which ->
preference.summary = "${items[which]}" preference.summary = items[which]
putPrefInt("default_theme", which) putPrefInt("default_theme", which)
when (which) { when (which) {
0 -> { 0 -> {
@ -180,7 +180,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
} }
private fun recreateActivities() { private fun recreateActivities() {
postEvent(Bus.RECREATE, "") postEvent(EventBus.RECREATE, "")
Handler().postDelayed({ activity?.recreate() }, 100L) Handler().postDelayed({ activity?.recreate() }, 100L)
} }

@ -9,7 +9,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.service.help.Download import io.legado.app.service.help.Download
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
@ -67,7 +67,7 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) {
} }
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<Boolean>(Bus.UP_DOWNLOAD) { observeEvent<Boolean>(EventBus.UP_DOWNLOAD) {
if (it) { if (it) {
menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp) menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp)
menu?.applyTint(this) menu?.applyTint(this)

@ -6,7 +6,6 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.help.ImageLoader
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover
@ -58,13 +57,7 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
} }
} }
} }
item.coverUrl.let { iv_cover.load(item.coverUrl, item.name, item.author)
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
onClick { onClick {
callBack.showBookInfo(item.toBook()) callBack.showBookInfo(item.toBook())
} }

@ -21,7 +21,7 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application
execute { execute {
val sourceUrl = intent.getStringExtra("sourceUrl") val sourceUrl = intent.getStringExtra("sourceUrl")
exploreUrl = intent.getStringExtra("exploreUrl") exploreUrl = intent.getStringExtra("exploreUrl")
if (bookSource == null) { if (bookSource == null && sourceUrl != null) {
bookSource = App.db.bookSourceDao().getBookSource(sourceUrl) bookSource = App.db.bookSourceDao().getBookSource(sourceUrl)
} }
explore() explore()

@ -64,7 +64,7 @@ class FileAdapter(context: Context, val callBack: CallBack) :
fileParent.icon = upIcon fileParent.icon = upIcon
fileParent.name = DIR_PARENT fileParent.name = DIR_PARENT
fileParent.size = 0 fileParent.size = 0
fileParent.path = File(path).parent fileParent.path = File(path).parent ?: ""
data.add(fileParent) data.add(fileParent)
} }
currentPath?.let { currentPath -> currentPath?.let { currentPath ->

@ -15,6 +15,7 @@ import java.util.*
class PathAdapter(context: Context, val callBack: CallBack) : class PathAdapter(context: Context, val callBack: CallBack) :
SimpleRecyclerAdapter<String>(context, R.layout.item_path_filepicker) { SimpleRecyclerAdapter<String>(context, R.layout.item_path_filepicker) {
private val paths = LinkedList<String>() private val paths = LinkedList<String>()
@Suppress("DEPRECATION")
private val sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath private val sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath
private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW()) private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW())

@ -34,13 +34,13 @@ object FileUtils {
* 将目录分隔符统一为平台默认的分隔符并为目录结尾添加分隔符 * 将目录分隔符统一为平台默认的分隔符并为目录结尾添加分隔符
*/ */
fun separator(path: String): String { fun separator(path: String): String {
var path = path var path1 = path
val separator = File.separator val separator = File.separator
path = path.replace("\\", separator) path1 = path1.replace("\\", separator)
if (!path.endsWith(separator)) { if (!path1.endsWith(separator)) {
path += separator path1 += separator
} }
return path return path1
} }
fun closeSilently(c: Closeable?) { fun closeSilently(c: Closeable?) {
@ -62,7 +62,7 @@ object FileUtils {
startDirPath: String, startDirPath: String,
excludeDirs: Array<String?>? = null, @SortType sortType: Int = BY_NAME_ASC excludeDirs: Array<String?>? = null, @SortType sortType: Int = BY_NAME_ASC
): Array<File?> { ): Array<File?> {
var excludeDirs = excludeDirs var excludeDirs1 = excludeDirs
val dirList = ArrayList<File>() val dirList = ArrayList<File>()
val startDir = File(startDirPath) val startDir = File(startDirPath)
if (!startDir.isDirectory) { if (!startDir.isDirectory) {
@ -75,12 +75,12 @@ object FileUtils {
f.isDirectory f.isDirectory
}) })
?: return arrayOfNulls(0) ?: return arrayOfNulls(0)
if (excludeDirs == null) { if (excludeDirs1 == null) {
excludeDirs = arrayOfNulls(0) excludeDirs1 = arrayOfNulls(0)
} }
for (dir in dirs) { for (dir in dirs) {
val file = dir.absoluteFile val file = dir.absoluteFile
if (!excludeDirs.contentDeepToString().contains(file.name)) { if (!excludeDirs1.contentDeepToString().contains(file.name)) {
dirList.add(file) dirList.add(file)
} }
} }
@ -191,7 +191,7 @@ object FileUtils {
*/ */
fun listFiles(startDirPath: String, allowExtensions: Array<String?>): Array<File?>? { fun listFiles(startDirPath: String, allowExtensions: Array<String?>): Array<File?>? {
val file = File(startDirPath) val file = File(startDirPath)
return file.listFiles { dir, name -> return file.listFiles { _, name ->
//返回当前目录所有以某些扩展名结尾的文件 //返回当前目录所有以某些扩展名结尾的文件
val extension = getExtension(name) val extension = getExtension(name)
allowExtensions.contentDeepToString().contains(extension) allowExtensions.contentDeepToString().contains(extension)
@ -294,10 +294,8 @@ object FileUtils {
bis.close() bis.close()
bos.close() bos.close()
} else if (src.isDirectory) { } else if (src.isDirectory) {
val files = src.listFiles()
tar.mkdirs() tar.mkdirs()
for (file in files) { src.listFiles()?.forEach { file ->
copy(file.absoluteFile, File(tar.absoluteFile, file.name)) copy(file.absoluteFile, File(tar.absoluteFile, file.name))
} }
} }
@ -399,18 +397,16 @@ object FileUtils {
fun writeBytes(filepath: String, data: ByteArray): Boolean { fun writeBytes(filepath: String, data: ByteArray): Boolean {
val file = File(filepath) val file = File(filepath)
var fos: FileOutputStream? = null var fos: FileOutputStream? = null
try { return try {
if (!file.exists()) { if (!file.exists()) {
file.parentFile?.mkdirs()
file.parentFile.mkdirs()
file.createNewFile() file.createNewFile()
} }
fos = FileOutputStream(filepath) fos = FileOutputStream(filepath)
fos.write(data) fos.write(data)
return true true
} catch (e: IOException) { } catch (e: IOException) {
return false false
} finally { } finally {
closeSilently(fos) closeSilently(fos)
} }
@ -422,16 +418,16 @@ object FileUtils {
fun appendText(path: String, content: String): Boolean { fun appendText(path: String, content: String): Boolean {
val file = File(path) val file = File(path)
var writer: FileWriter? = null var writer: FileWriter? = null
try { return try {
if (!file.exists()) { if (!file.exists()) {
file.createNewFile() file.createNewFile()
} }
writer = FileWriter(file, true) writer = FileWriter(file, true)
writer.write(content) writer.write(content)
return true true
} catch (e: IOException) { } catch (e: IOException) {
return false false
} finally { } finally {
closeSilently(writer) closeSilently(writer)
} }

@ -3,9 +3,8 @@ package io.legado.app.ui.importbook
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book import io.legado.app.model.localBook.LocalBook
class ImportBookViewModel(application: Application) : BaseViewModel(application) { class ImportBookViewModel(application: Application) : BaseViewModel(application) {
@ -14,18 +13,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
execute { execute {
uriList.forEach { uriStr -> uriList.forEach { uriStr ->
DocumentFile.fromSingleUri(context, Uri.parse(uriStr))?.let { doc -> DocumentFile.fromSingleUri(context, Uri.parse(uriStr))?.let { doc ->
doc.name?.let { fileName -> LocalBook.importFile(doc)
val str = fileName.substringBeforeLast(".")
var name = str.substringBefore("作者")
val author = str.substringAfter("作者")
val smhStart = name.indexOf("")
val smhEnd = name.indexOf("")
if (smhStart != -1 && smhEnd != -1) {
name = (name.substring(smhStart + 1, smhEnd))
}
val book = Book(bookUrl = uriStr, name = name, author = author)
App.db.bookDao().insert(book)
}
} }
} }
}.onFinally { }.onFinally {

@ -14,7 +14,7 @@ import io.legado.app.App
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
@ -77,8 +77,8 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
private fun upVersion() { private fun upVersion() {
if (getPrefInt("versionCode") != App.INSTANCE.versionCode) { if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) {
putPrefInt("versionCode", App.INSTANCE.versionCode) putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
UpdateLog().show(supportFragmentManager, "updateLog") UpdateLog().show(supportFragmentManager, "updateLog")
} }
@ -86,6 +86,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
view_pager_main.hideSoftInput()
pagePosition = position pagePosition = position
when (position) { when (position) {
0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true 0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true
@ -143,10 +144,10 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<String>(Bus.RECREATE) { observeEvent<String>(EventBus.RECREATE) {
recreate() recreate()
} }
observeEvent<String>(Bus.SHOW_RSS) { observeEvent<String>(EventBus.SHOW_RSS) {
bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
upFragmentList() upFragmentList()
view_pager_main.adapter?.notifyDataSetChanged() view_pager_main.adapter?.notifyDataSetChanged()

@ -3,7 +3,7 @@ package io.legado.app.ui.main
import android.app.Application import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
@ -24,14 +24,14 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
synchronized(this) { synchronized(this) {
updateList.add(book.bookUrl) updateList.add(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl) postEvent(EventBus.UP_BOOK, book.bookUrl)
} }
WebBook(bookSource).getChapterList(book) WebBook(bookSource).getChapterList(book)
.timeout(300000) .timeout(300000)
.onSuccess(IO) { .onSuccess(IO) {
synchronized(this) { synchronized(this) {
updateList.remove(book.bookUrl) updateList.remove(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl) postEvent(EventBus.UP_BOOK, book.bookUrl)
} }
it?.let { it?.let {
App.db.bookDao().update(book) App.db.bookDao().update(book)
@ -42,7 +42,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
.onError { .onError {
synchronized(this) { synchronized(this) {
updateList.remove(book.bookUrl) updateList.remove(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl) postEvent(EventBus.UP_BOOK, book.bookUrl)
} }
it.printStackTrace() it.printStackTrace()
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.main.bookshelf package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -13,16 +14,20 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.*
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.view.ATEAutoCompleteTextView
import io.legado.app.ui.book.arrange.ArrangeBookActivity
import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.download.DownloadActivity import io.legado.app.ui.download.DownloadActivity
import io.legado.app.ui.importbook.ImportBookActivity import io.legado.app.ui.importbook.ImportBookActivity
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.fragment_bookshelf.* import kotlinx.android.synthetic.main.fragment_bookshelf.*
import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.android.synthetic.main.view_tab_layout.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
@ -60,10 +65,8 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog") .show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>() R.id.menu_add_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> { R.id.menu_add_url -> addBookByUrl()
} R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity>()
R.id.menu_arrange_bookshelf -> {
}
R.id.menu_download -> startActivity<DownloadActivity>() R.id.menu_download -> startActivity<DownloadActivity>()
} }
} }
@ -85,7 +88,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position -> TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position ->
tab.text = bookGroups[position].groupName tab.text = bookGroups[position].groupName
}.attach() }.attach()
observeEvent<Int>(Bus.UP_TABS) { observeEvent<Int>(EventBus.UP_TABS) {
tab_layout.getTabAt(it)?.select() tab_layout.getTabAt(it)?.select()
} }
} }
@ -145,6 +148,25 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
} }
@SuppressLint("InflateParams")
private fun addBookByUrl() {
requireContext()
.alert(titleResource = R.string.add_book_url) {
var editText: ATEAutoCompleteTextView? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view
}
}
okButton {
editText?.text?.toString()?.let {
viewModel.addBookByUrl(it)
}
}
noButton { }
}.show().applyTint()
}
override fun onTabReselected(tab: TabLayout.Tab?) { override fun onTabReselected(tab: TabLayout.Tab?) {
} }

@ -2,8 +2,14 @@ package io.legado.app.ui.main.bookshelf
import android.app.Application import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseViewModel 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.BookGroup
import io.legado.app.data.entities.BookSource
import io.legado.app.model.WebBook
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.Dispatchers.IO
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -31,4 +37,54 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun addBookByUrl(bookUrls: String) {
var successCount = 0
execute {
var hasBookUrlPattern: List<BookSource>? = null
val urls = bookUrls.split("\n")
for (url in urls) {
val bookUrl = url.trim()
if (bookUrl.isEmpty()) continue
if (App.db.bookDao().getBook(bookUrl) != null) continue
val baseUrl = NetworkUtils.getBaseUrl(bookUrl) ?: continue
var source = App.db.bookSourceDao().getBookSource(baseUrl)
if (source == null) {
if (hasBookUrlPattern == null) {
hasBookUrlPattern = App.db.bookSourceDao().hasBookUrlPattern
}
hasBookUrlPattern.forEach { bookSource ->
if (bookUrl.matches(bookSource.bookUrlPattern!!.toRegex())) {
source = bookSource
return@forEach
}
}
}
source?.let { bookSource ->
val book = Book(
bookUrl = bookUrl,
origin = bookSource.bookSourceUrl,
originName = bookSource.bookSourceName
)
WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) {
it?.let { book ->
App.db.bookDao().insert(book)
successCount++
}
}.onError {
throw Exception(it.localizedMessage)
}
}
}
}.onSuccess {
if (successCount > 0) {
toast(R.string.success)
} else {
toast("ERROR")
}
}.onError {
toast(it.localizedMessage ?: "ERROR")
}
}
} }

@ -4,7 +4,7 @@ import android.content.Context
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
abstract class BooksAdapter(context: Context, layoutId: Int) : abstract class BaseBooksAdapter(context: Context, layoutId: Int) :
SimpleRecyclerAdapter<Book>(context, layoutId) { SimpleRecyclerAdapter<Book>(context, layoutId) {
fun notification(bookUrl: String) { fun notification(bookUrl: String) {

@ -5,7 +5,6 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.ImageLoader
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.utils.invisible import io.legado.app.utils.invisible
import kotlinx.android.synthetic.main.item_bookshelf_grid.view.* import kotlinx.android.synthetic.main.item_bookshelf_grid.view.*
@ -13,7 +12,7 @@ import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
class BooksAdapterGrid(context: Context, private val callBack: CallBack) : class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
BooksAdapter(context, R.layout.item_bookshelf_grid) { BaseBooksAdapter(context, R.layout.item_bookshelf_grid) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
@ -21,13 +20,7 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
ATH.applyBackgroundTint(this) ATH.applyBackgroundTint(this)
tv_name.text = item.name tv_name.text = item.name
bv_author.text = item.author bv_author.text = item.author
item.getDisplayCover()?.let { iv_cover.load(item.getDisplayCover(), item.name, item.author)
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
onClick { callBack.open(item) } onClick { callBack.open(item) }
onLongClick { onLongClick {
callBack.openBookInfo(item) callBack.openBookInfo(item)

@ -5,7 +5,6 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.ImageLoader
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.utils.invisible import io.legado.app.utils.invisible
import kotlinx.android.synthetic.main.item_bookshelf_list.view.* import kotlinx.android.synthetic.main.item_bookshelf_list.view.*
@ -13,7 +12,7 @@ import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
class BooksAdapterList(context: Context, private val callBack: CallBack) : class BooksAdapterList(context: Context, private val callBack: CallBack) :
BooksAdapter(context, R.layout.item_bookshelf_list) { BaseBooksAdapter(context, R.layout.item_bookshelf_list) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
@ -23,13 +22,7 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) :
tv_author.text = item.author tv_author.text = item.author
tv_read.text = item.durChapterTitle tv_read.text = item.durChapterTitle
tv_last.text = item.latestChapterTitle tv_last.text = item.latestChapterTitle
item.getDisplayCover()?.let { iv_cover.load(item.getDisplayCover(), item.name, item.author)
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
onClick { callBack.open(item) } onClick { callBack.open(item) }
onLongClick { onLongClick {
callBack.openBookInfo(item) callBack.openBookInfo(item)

@ -23,6 +23,7 @@ class BooksDiffCallBack(private val oldItems: List<Book>, private val newItems:
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
return oldItem.name == newItem.name return oldItem.name == newItem.name
&& oldItem.author == newItem.author
&& oldItem.durChapterTitle == newItem.durChapterTitle && oldItem.durChapterTitle == newItem.durChapterTitle
&& oldItem.latestChapterTitle == newItem.latestChapterTitle && oldItem.latestChapterTitle == newItem.latestChapterTitle
&& oldItem.getDisplayCover() == newItem.getDisplayCover() && oldItem.getDisplayCover() == newItem.getDisplayCover()

@ -11,7 +11,7 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.constant.Bus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
@ -29,7 +29,7 @@ import org.jetbrains.anko.startActivity
class BooksFragment : BaseFragment(R.layout.fragment_books), class BooksFragment : BaseFragment(R.layout.fragment_books),
BooksAdapter.CallBack { BaseBooksAdapter.CallBack {
companion object { companion object {
fun newInstance(position: Int, groupId: Int): BooksFragment { fun newInstance(position: Int, groupId: Int): BooksFragment {
@ -43,7 +43,7 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
} }
private lateinit var activityViewModel: MainViewModel private lateinit var activityViewModel: MainViewModel
private lateinit var booksAdapter: BooksAdapter private lateinit var booksAdapter: BaseBooksAdapter
private var bookshelfLiveData: LiveData<List<Book>>? = null private var bookshelfLiveData: LiveData<List<Book>>? = null
private var position = 0 private var position = 0
private var groupId = -1 private var groupId = -1
@ -56,7 +56,7 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
} }
initRecyclerView() initRecyclerView()
upRecyclerData() upRecyclerData()
observeEvent<String>(Bus.UP_BOOK) { observeEvent<String>(EventBus.UP_BOOK) {
booksAdapter.notification(it) booksAdapter.notification(it)
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save