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. 4
      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. 21
      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. 109
      app/src/main/java/io/legado/app/help/BookHelp.kt
  25. 14
      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. 112
      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. 22
      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. 54
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  41. 8
      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. 101
      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. 55
      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. 64
      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. 48
      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. 71
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  83. 53
      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
## 阅读3.0
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg

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

@ -3,8 +3,23 @@
* 旧版数据导入教程:
* 先在旧版阅读(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**
* 正在写本地导入,还没写完
* 修复搜索闪退,因为默认线程为0了
**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.content.Intent
import android.os.IBinder
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext
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? {
return null
}
override fun onDestroy() {
super.onDestroy()
cancel()

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

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

@ -1,12 +1,17 @@
package io.legado.app.constant
object PreferKey {
const val versionCode = "versionCode"
const val themeMode = "themeMode"
const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar"
const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar"
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 nextKey = "nextKeyCode"
const val showRss = "showRss"
@ -21,5 +26,8 @@ object PreferKey {
const val backupPath = "backupUri"
const val threadCount = "threadCount"
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(
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssStar::class],
version = 6,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class],
version = 8,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -50,4 +51,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun rssArticleDao(): RssArticleDao
abstract fun rssStarDao(): RssStarDao
abstract fun cookieDao(): CookieDao
abstract fun txtTocRule(): TxtTocRuleDao
}

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

@ -45,7 +45,7 @@ interface BookDao {
val hasUpdateBooks: List<Book>
@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")
val lastReadBook: Book?
@ -60,10 +60,10 @@ interface BookDao {
fun insert(vararg book: Book)
@Update
fun update(vararg books: Book)
fun update(vararg book: Book)
@Query("delete from books where bookUrl = :bookUrl")
fun delete(bookUrl: String)
@Delete
fun delete(vararg book: Book)
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
fun upProgress(bookUrl: String, pos: Int)

@ -19,8 +19,8 @@ interface BookGroupDao {
@get:Query("SELECT MAX(`order`) FROM book_groups")
val maxOrder: Int
@Query("SELECT * FROM book_groups ORDER BY `order`")
fun all(): List<BookGroup>
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
val all: List<BookGroup>
@Insert(onConflict = OnConflictStrategy.REPLACE)
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")
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")
fun observeFind(): DataSource.Factory<Int, BookSource>
@ -53,6 +38,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
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 = ''")
val noGroup: List<BookSource>
@ -75,7 +63,7 @@ interface BookSourceDao {
fun update(vararg bookSource: BookSource)
@Delete
fun delete(bookSource: BookSource)
fun delete(vararg bookSource: BookSource)
@Query("delete from book_sources where bookSourceUrl = :key")
fun delete(key: String)

@ -3,6 +3,7 @@ package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssReadRecord
@Dao
interface RssArticleDao {
@ -10,7 +11,11 @@ interface RssArticleDao {
@Query("select * from rssArticles where origin = :origin and link = :link")
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>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
@ -24,4 +29,9 @@ interface RssArticleDao {
@Query("delete from rssArticles where origin = :origin")
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
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")
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")
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)
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 kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.nio.charset.Charset
import kotlin.math.max
@Parcelize
@ -19,15 +20,15 @@ data class Book(
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var originName: String = "", //书源名称 or 本地书籍文件名
var name: String = "", // 书籍名称(书源获取)
var author: String = "", // 作者名称(书源获取)
override var kind: String? = null, // 分类信息(书源获取)
var customTag: String? = null, // 分类信息(用户修改)
var coverUrl: String? = null, // 封面Url(书源获取)
var customCoverUrl: String? = null, // 封面Url(用户修改)
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var intro: String? = null, // 简介内容(书源获取)
var customIntro: String? = null, // 简介内容(用户修改)
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
var type: Int = 0, // @BookType
var group: Int = 0, // 自定义分组索引号
@ -52,6 +53,10 @@ data class Book(
return origin == BookType.local
}
fun isTxt(): Boolean {
return isLocalBook() && originName.endsWith(".txt", true)
}
@Ignore
@IgnoredOnParcel
override var variableMap: HashMap<String, String>? = null
@ -81,6 +86,10 @@ data class Book(
variable = GSON.toJson(variableMap)
}
fun fileCharset(): Charset {
return charset(charset ?: "UTF-8")
}
fun toSearchBook(): SearchBook {
return SearchBook(
name = name,

@ -47,6 +47,18 @@ data class BookSource(
var ruleToc: String? = null, // 目录页规则
var ruleContent: String? = null // 正文页规则
) : 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
@IgnoredOnParcel
private var searchRuleV: SearchRule? = null
@ -126,6 +138,16 @@ data class BookSource(
return contentRuleV!!
}
fun addGroup(group: String) {
bookSourceGroup?.let {
if (!it.contains(group)) {
bookSourceGroup = "$it;$group"
}
} ?: let {
bookSourceGroup = group
}
}
fun getExploreKinds(): ArrayList<ExploreKind>? {
val exploreKinds = arrayListOf<ExploreKind>()
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
get() {
return when (App.INSTANCE.getPrefString("themeMode", "0")) {
return when (App.INSTANCE.getPrefString(PreferKey.themeMode, "0")) {
"1" -> false
"2" -> true
else -> App.INSTANCE.sysIsDarkMode()
@ -16,9 +16,9 @@ object AppConfig {
}
set(value) {
if (value) {
App.INSTANCE.putPrefString("themeMode", "2")
App.INSTANCE.putPrefString(PreferKey.themeMode, "2")
} 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
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.BookChapter
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.utils.*
import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File
@ -14,7 +15,7 @@ import kotlin.math.min
object BookHelp {
private const val cacheFolderName = "book_cache"
private var downloadPath: String =
var downloadPath: String =
App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
@ -37,7 +38,7 @@ object BookHelp {
}
fun clearCache() {
if (downloadUri.isDocumentUri(App.INSTANCE)) {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)
?.findFile(cacheFolderName)
?.delete()
@ -54,15 +55,17 @@ object BookHelp {
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
if (downloadUri.isDocumentUri(App.INSTANCE)) {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.listFiles()?.forEach {
if (it.name?.startsWith(String.format("%05d", bookChapter.index)) == true) {
it.delete()
return@forEach
)?.uri?.let { uri ->
DocumentUtils.listFiles(App.INSTANCE, uri).forEach {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
DocumentFile.fromSingleUri(App.INSTANCE, it.uri)?.delete()
return@forEach
}
}
}
DocumentUtils.createFileIfNotExist(
@ -90,7 +93,7 @@ object BookHelp {
}
fun getChapterCount(book: Book): Int {
if (downloadUri.isDocumentUri(App.INSTANCE)) {
if (downloadPath.isContentPath()) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.createFolderIfNotExist(
root,
@ -107,61 +110,77 @@ object BookHelp {
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
if (downloadUri.isDocumentUri(App.INSTANCE)) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists(
root,
when {
book.isLocalBook() -> {
return true
}
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.exists(
root,
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
}
else -> {
return FileUtils.exists(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
} else {
return FileUtils.exists(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
}
return false
}
fun getContent(book: Book, bookChapter: BookChapter): String? {
if (downloadUri.isDocumentUri(App.INSTANCE)) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile("${bookChapterName(bookChapter)}.nb")
?.uri?.readText(App.INSTANCE)
when {
book.isLocalBook() -> {
return AnalyzeTxtFile.getContent(book, bookChapter)
}
} else {
val file = FileUtils.getFile(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
return DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile("${bookChapterName(bookChapter)}.nb")
?.uri?.readText(App.INSTANCE)
}
}
else -> {
val file = FileUtils.getFile(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
if (file.exists()) {
return file.readText()
}
}
}
return null
}
fun delContent(book: Book, bookChapter: BookChapter) {
if (downloadUri.isDocumentUri(App.INSTANCE)) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
when {
book.isLocalBook() -> return
downloadPath.isContentPath() -> {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root ->
DocumentUtils.getDirDocument(
root,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile("${bookChapterName(bookChapter)}.nb")
?.delete()
}
}
else -> {
FileUtils.createFileIfNotExist(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.findFile("${bookChapterName(bookChapter)}.nb")
?.delete()
).delete()
}
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
}
}

@ -12,13 +12,15 @@ import java.io.File
object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> {
if (path?.startsWith("http", true) == true) {
return Glide.with(context).load(path)
return when {
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> {

@ -36,6 +36,7 @@ object Backup {
"bookGroup.json",
"bookSource.json",
"rssSource.json",
"rssStar.json",
"replaceRule.json",
ReadBookConfig.readConfigFileName,
"config.xml"
@ -44,41 +45,12 @@ object Backup {
suspend fun backup(context: Context, uri: Uri?) {
withContext(IO) {
App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.createFileIfNotExist(backupPath + File.separator + "bookshelf.json")
.writeText(json)
}
}
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)
}
}
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)
writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath)
writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath)
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.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) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
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.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.jayway.jsonpath.Configuration
import com.jayway.jsonpath.JsonPath
import com.jayway.jsonpath.Option
import com.jayway.jsonpath.ParseContext
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.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.Main
import kotlinx.coroutines.GlobalScope
@ -49,51 +51,23 @@ object Restore {
suspend fun restore(path: String) {
withContext(IO) {
try {
val file = FileUtils.createFileIfNotExist(path + File.separator + "bookshelf.json")
val json = file.readText()
GSON.fromJsonArray<Book>(json)?.let {
App.db.bookDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<Book>(path, "bookshelf.json")?.let {
App.db.bookDao().insert(*it.toTypedArray())
}
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())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<BookGroup>(path, "bookGroup.json")?.let {
App.db.bookGroupDao().insert(*it.toTypedArray())
}
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())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<BookSource>(path, "bookSource.json")?.let {
App.db.bookSourceDao().insert(*it.toTypedArray())
}
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())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<RssSource>(path, "rssSource.json")?.let {
App.db.rssSourceDao().insert(*it.toTypedArray())
}
try {
val file =
FileUtils.createFileIfNotExist(path + File.separator + "replaceRule.json")
val json = file.readText()
GSON.fromJsonArray<ReplaceRule>(json)?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray())
}
} catch (e: Exception) {
e.printStackTrace()
fileToListT<RssStar>(path, "rssStar.json")?.let {
App.db.rssStarDao().insert(*it.toTypedArray())
}
fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray())
}
try {
val file =
@ -117,11 +91,23 @@ object Restore {
is String -> edit.putString(it.key, value)
else -> Unit
}
edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode)
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) {
GlobalScope.launch(IO) {
try {// 导入书架
@ -169,43 +155,7 @@ object Restore {
}
fun importOldBookshelf(json: String): Int {
val books = mutableListOf<Book>()
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)
}
val books = OldBook.toNewBook(json)
App.db.bookDao().insert(*books.toTypedArray())
return books.size
}

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

@ -254,11 +254,10 @@ object TintHelper {
if (!skipIndeterminate)
progressBar.indeterminateTintList = sl
} else {
val mode = PorterDuff.Mode.SRC_IN
if (!skipIndeterminate && progressBar.indeterminateDrawable != null)
progressBar.indeterminateDrawable.setColorFilter(color, mode)
progressBar.indeterminateDrawable.setTint(color)
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.net.Uri
import io.legado.app.App
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 {
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
fun analyze(context: Context, book: Book) {
context.contentResolver.openInputStream(Uri.parse(book.bookUrl))?.use { stream ->
val rawByteArray = ByteArray(2000)
stream.read(rawByteArray)
book.charset = EncodingDetect.getJavaEncode(rawByteArray)
//获取文件中的数据到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
}
}
}
//block的偏移点
curOffset += length.toLong()
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)
}
}
}

@ -51,17 +51,17 @@ object BookChapterList {
ruleUrl = nextUrl, book = book, headerMapF = bookSource.getHeaderMap()
).getResponseAwait()
.body?.let { nextBody ->
chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule,
book, bookSource, log = false
)
nextUrl = if (chapterData.nextUrl.isNotEmpty())
chapterData.nextUrl[0]
else ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule,
book, bookSource, log = false
)
nextUrl = if (chapterData.nextUrl.isNotEmpty())
chapterData.nextUrl[0]
else ""
chapterData.chapterList?.let {
chapterList.addAll(it)
}
}
}
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}")
} else if (chapterData.nextUrl.size > 1) {
@ -100,6 +100,8 @@ object BookChapterList {
item.index = index
}
book.latestChapterTitle = chapterList.last().title
book.durChapterTitle =
chapterList.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle
if (book.totalChapterNum < chapterList.size) {
book.lastCheckCount = chapterList.size - book.totalChapterNum
}

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

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

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

@ -2,28 +2,77 @@ package io.legado.app.service
import android.content.Intent
import androidx.core.app.NotificationCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.Action
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.coroutine.Coroutine
import io.legado.app.model.WebBook
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() {
private var sourceList: List<BookSource>? = null
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null
private var idsCount = 0
private val unCheckIds = LinkedHashSet<String>()
override fun onCreate() {
super.onCreate()
updateNotification(0, getString(R.string.start))
}
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)
}
override fun 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(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, Action.stop)
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
)
sourceList?.let {
builder.setProgress(it.size, state, false)
}
builder.setProgress(idsCount, state, false)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
startForeground(112202, notification)

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

@ -2,12 +2,15 @@ package io.legado.app.service
import android.app.PendingIntent
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.http.HttpHelper
import io.legado.app.help.http.api.HttpPostApi
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.Job
import kotlinx.coroutines.isActive
@ -37,6 +40,7 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onDestroy() {
super.onDestroy()
job?.cancel()
mediaPlayer.release()
}
@ -88,11 +92,12 @@ class HttpReadAloudService : BaseReadAloudService(),
@Synchronized
private fun playAudio(fd: FileDescriptor) {
if (playingIndex != nowSpeak && requestFocus()) {
playingIndex = nowSpeak
try {
mediaPlayer.reset()
mediaPlayer.setDataSource(fd)
mediaPlayer.prepareAsync()
playingIndex = nowSpeak
postEvent(EventBus.TTS_START, readAloudNumber + 1)
} catch (e: Exception) {
e.printStackTrace()
}
@ -106,8 +111,8 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun getAudioBody(content: String): Map<String, String> {
return mapOf(
Pair("tex", encodeTwo(content)),
Pair("spd", ((getPrefInt("ttsSpeechRate", 25) + 5) / 5).toString()),
Pair("per", getPrefString("ttsSpeechPer") ?: "0"),
Pair("spd", ((AppConfig.ttsSpeechRate + 5) / 10 + 4).toString()),
Pair("per", AppConfig.ttsSpeechPer),
Pair("cuid", "baidu_speech_demo"),
Pair("idx", "1"),
Pair("cod", "2"),
@ -135,20 +140,26 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun resumeReadAloud() {
super.resumeReadAloud()
mediaPlayer.start()
if (playingIndex == -1) {
play()
} else {
mediaPlayer.start()
}
}
/**
* 更新朗读速度
*/
override fun upSpeechRate(reset: Boolean) {
job?.cancel()
mediaPlayer.reset()
for (i in 0 until nowSpeak) {
contentList.removeAt(0)
}
nowSpeak = 0
mediaPlayer.stop()
playingIndex = -1
play()
downloadAudio()
}
/**
* 上一段
*/
override fun prevP() {
if (nowSpeak > 0) {
mediaPlayer.stop()
@ -158,6 +169,9 @@ class HttpReadAloudService : BaseReadAloudService(),
}
}
/**
* 下一段
*/
override fun nextP() {
if (nowSpeak < contentList.size - 1) {
mediaPlayer.stop()
@ -177,15 +191,27 @@ class HttpReadAloudService : BaseReadAloudService(),
ReadBook.moveToNextPage()
}
}
postEvent(Bus.TTS_START, readAloudNumber + 1)
postEvent(EventBus.TTS_START, readAloudNumber + 1)
}
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
}
override fun onCompletion(mp: MediaPlayer?) {
LogUtils.d("播放完成", contentList[nowSpeak])
readAloudNumber += contentList[nowSpeak].length + 1
if (nowSpeak < contentList.lastIndex) {
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 io.legado.app.R
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.MediaHelp
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent
import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
@ -96,7 +96,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
textToSpeech = TextToSpeech(this, this)
}
} 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()
}
}
postEvent(Bus.TTS_START, readAloudNumber + 1)
postEvent(EventBus.TTS_START, readAloudNumber + 1)
}
override fun onDone(s: String) {
@ -169,7 +169,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) {
pageIndex++
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.R
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.Bus
import io.legado.app.constant.EventBus
import io.legado.app.help.IntentHelp
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getPrefInt
@ -32,7 +32,7 @@ class WebService : BaseService() {
fun stop(context: Context) {
if (isRun) {
val intent = Intent(context, WebService::class.java)
intent.action = Action.stop
intent.action = IntentAction.stop
context.startService(intent)
}
}
@ -56,12 +56,12 @@ class WebService : BaseService() {
if (webSocketServer?.isAlive == true) {
webSocketServer?.stop()
}
postEvent(Bus.WEB_SERVICE_STOP, true)
postEvent(EventBus.WEB_SERVICE_STOP, true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
Action.stop -> stopSelf()
IntentAction.stop -> stopSelf()
else -> upWebServer()
}
return super.onStartCommand(intent, flags, startId)
@ -115,7 +115,7 @@ class WebService : BaseService() {
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<WebService>(this, Action.stop)
IntentHelp.servicePendingIntent<WebService>(this, IntentAction.stop)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()

@ -3,7 +3,7 @@ package io.legado.app.service.help
import android.content.Context
import android.content.Intent
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.data.entities.Book
import io.legado.app.model.WebBook
@ -27,14 +27,14 @@ object AudioPlay {
fun play(context: Context) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.play
intent.action = IntentAction.play
context.startService(intent)
}
fun pause(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.pause
intent.action = IntentAction.pause
context.startService(intent)
}
}
@ -42,7 +42,7 @@ object AudioPlay {
fun resume(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.resume
intent.action = IntentAction.resume
context.startService(intent)
}
}
@ -50,7 +50,7 @@ object AudioPlay {
fun stop(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.stop
intent.action = IntentAction.stop
context.startService(intent)
}
}
@ -58,7 +58,7 @@ object AudioPlay {
fun adjustSpeed(context: Context, adjust: Float) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.adjustSpeed
intent.action = IntentAction.adjustSpeed
intent.putExtra("adjust", adjust)
context.startService(intent)
}
@ -67,7 +67,7 @@ object AudioPlay {
fun adjustProgress(context: Context, position: Int) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.adjustProgress
intent.action = IntentAction.adjustProgress
intent.putExtra("position", position)
context.startService(intent)
}
@ -76,7 +76,7 @@ object AudioPlay {
fun prev(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.prev
intent.action = IntentAction.prev
context.startService(intent)
}
}
@ -84,7 +84,7 @@ object AudioPlay {
fun next(context: Context) {
if (AudioPlayService.isRun) {
val intent = Intent(context, AudioPlayService::class.java)
intent.action = Action.next
intent.action = IntentAction.next
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.Intent
import io.legado.app.constant.Action
import io.legado.app.constant.IntentAction
import io.legado.app.service.DownloadService
object Download {
fun start(context: Context, bookUrl: String, start: Int, end: Int) {
Intent(context, DownloadService::class.java).let {
it.action = Action.start
it.action = IntentAction.start
it.putExtra("bookUrl", bookUrl)
it.putExtra("start", start)
it.putExtra("end", end)

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

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

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

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

@ -91,7 +91,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun changeTo(book1: Book) {
execute {
AudioPlay.book?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
withContext(Dispatchers.Main) {
@ -142,7 +142,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun removeFromBookshelf(success: (() -> Unit)?) {
execute {
AudioPlay.book?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
}.onSuccess {
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.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.noButton
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.getViewModel
import io.legado.app.utils.requestInputMethod
@ -77,7 +78,12 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
recycler_view.adapter = adapter
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
val diffResult =
DiffUtil.calculateDiff(GroupDiffCallBack(adapter.getItems(), it))
DiffUtil.calculateDiff(
GroupDiffCallBack(
adapter.getItems(),
it
)
)
adapter.setItems(it, false)
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.content.Context
@ -44,7 +44,10 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
fun show(manager: FragmentManager) {
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.IntentDataHelp
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.read.ReadBookActivity
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.chapterlist.ChapterListActivity
import io.legado.app.utils.getViewModel
@ -40,7 +42,8 @@ class BookInfoActivity :
VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, theme = Theme.Dark),
GroupSelectDialog.CallBack,
ChapterListAdapter.CallBack,
ChangeSourceDialog.CallBack {
ChangeSourceDialog.CallBack,
ChangeCoverDialog.CallBack {
private val requestCodeChapterList = 568
private val requestCodeSourceEdit = 562
@ -104,29 +107,14 @@ class BookInfoActivity :
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) {
showCover(book)
tv_name.text = book.name
tv_author.text = getString(R.string.author_show, book.author)
tv_origin.text = getString(R.string.origin_show, book.originName)
tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle)
tv_toc.text = getString(R.string.toc_s, book.latestChapterTitle)
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()
if (kinds.isEmpty()) {
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>) {
viewModel.bookData.value?.let {
if (it.durChapterIndex < chapterList.size) {
@ -184,6 +187,11 @@ class BookInfoActivity :
}
private fun initOnClick() {
iv_cover.onClick {
viewModel.bookData.value?.let {
ChangeCoverDialog.show(supportFragmentManager, it.name, it.author)
}
}
tv_read.onClick {
viewModel.bookData.value?.let {
readBook(it)
@ -276,6 +284,14 @@ class BookInfoActivity :
viewModel.changeTo(book)
}
override fun coverChangeTo(coverUrl: String) {
viewModel.bookData.value?.let {
it.coverUrl = coverUrl
viewModel.saveBook()
showCover(it)
}
}
override fun openChapter(chapter: BookChapter) {
if (chapter.index != viewModel.durChapterIndex) {
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.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) {
@ -55,26 +56,29 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
book: Book,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) {
if (book.isLocalBook()) return
execute {
isLoadingData.postValue(true)
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) {
it?.let {
bookData.postValue(book)
if (inBookshelf) {
App.db.bookDao().update(book)
if (book.isLocalBook()) {
loadChapter(book, changeDruChapterIndex)
} else {
isLoadingData.postValue(true)
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess(IO) {
it?.let {
bookData.postValue(book)
if (inBookshelf) {
App.db.bookDao().update(book)
}
loadChapter(it, changeDruChapterIndex)
}
loadChapter(it, changeDruChapterIndex)
}.onError {
isLoadingData.postValue(false)
toast(R.string.error_get_book_info)
}
}.onError {
isLoadingData.postValue(false)
toast(R.string.error_get_book_info)
}
} ?: let {
isLoadingData.postValue(false)
toast(R.string.error_no_source)
} ?: let {
isLoadingData.postValue(false)
toast(R.string.error_no_source)
}
}
}
}
@ -85,33 +89,46 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
) {
execute {
isLoadingData.postValue(true)
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) {
it?.let {
if (it.isNotEmpty()) {
if (inBookshelf) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
if (changeDruChapterIndex == null) {
chapterListData.postValue(it)
isLoadingData.postValue(false)
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 ->
WebBook(bookSource).getChapterList(book, this)
.onSuccess(IO) {
it?.let {
if (it.isNotEmpty()) {
if (inBookshelf) {
App.db.bookDao().update(book)
App.db.bookChapterDao().insert(*it.toTypedArray())
}
if (changeDruChapterIndex == null) {
chapterListData.postValue(it)
isLoadingData.postValue(false)
} else {
changeDruChapterIndex(it)
}
} else {
changeDruChapterIndex(it)
isLoadingData.postValue(false)
toast(R.string.chapter_list_empty)
}
} else {
isLoadingData.postValue(false)
toast(R.string.chapter_list_empty)
}
}.onError {
isLoadingData.postValue(false)
toast(R.string.error_get_chapter_list)
}
}.onError {
isLoadingData.postValue(false)
toast(R.string.error_get_chapter_list)
}
} ?: let {
isLoadingData.postValue(false)
toast(R.string.error_no_source)
} ?: let {
isLoadingData.postValue(false)
toast(R.string.error_no_source)
}
}
}
}
@ -120,7 +137,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
execute {
if (inBookshelf) {
bookData.value?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
App.db.bookDao().insert(book)
}
@ -185,7 +202,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
fun delBook(success: (() -> Unit)?) {
execute {
bookData.value?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
inBookshelf = false
}.onSuccess {

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

@ -13,13 +13,15 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.App
import io.legado.app.R
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.Status
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 requestCodeEditSource = 111
private val requestCodeReplace = 312
private var menu: Menu? = null
override val viewModel: ReadBookViewModel
get() = getViewModel(ReadBookViewModel::class.java)
@ -85,7 +88,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
initView()
upScreenTimeOut()
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)
}
@ -146,6 +152,33 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
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.durPageIndex,
textChapter!!.title,
editContent)
editContent
)
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_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)
}
@ -375,7 +416,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
seek_read_page.progress = ReadBook.durPageIndex
}
override fun showMenu() {
override fun showMenuBar() {
read_menu.runMenuIn()
}
@ -457,12 +498,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
when (dialogId) {
TEXT_COLOR -> {
setTextColor(color)
postEvent(Bus.UP_CONFIG, false)
postEvent(EventBus.UP_CONFIG, false)
}
BG_COLOR -> {
setBg(0, "#${color.hexString}")
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() {
super.observeLiveBus()
observeEvent<Int>(Bus.ALOUD_STATE) {
observeEvent<Int>(EventBus.ALOUD_STATE) {
if (it == Status.STOP || it == Status.PAUSE) {
ReadBook.curTextChapter?.let { textChapter ->
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<Int>(Bus.BATTERY_CHANGED) { page_view.upBattery(it) }
observeEvent<BookChapter>(Bus.OPEN_CHAPTER) {
observeEvent<String>(EventBus.TIME_CHANGED) { page_view.upTime() }
observeEvent<Int>(EventBus.BATTERY_CHANGED) { page_view.upBattery(it) }
observeEvent<BookChapter>(EventBus.OPEN_CHAPTER) {
viewModel.openChapter(it.index, ReadBook.durPageIndex)
page_view.upContent()
}
observeEvent<Boolean>(Bus.MEDIA_BUTTON) {
observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
if (it) {
onClickReadAloud()
} else {
ReadBook.readAloud(!BaseReadAloudService.pause)
}
}
observeEvent<Boolean>(Bus.UP_CONFIG) {
observeEvent<Boolean>(EventBus.UP_CONFIG) {
upSystemUiVisibility()
content_view.upStyle()
page_view.upBg()
@ -544,7 +585,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upContent()
}
}
observeEventSticky<Int>(Bus.TTS_START) { chapterStart ->
observeEventSticky<Int>(EventBus.TTS_START) { chapterStart ->
launch(IO) {
if (BaseReadAloudService.isPlay()) {
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")
}
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.IntentDataHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.AnalyzeTxtFile
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
@ -79,41 +80,55 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) {
execute {
ReadBook.webBook?.getBookInfo(book, this)
?.onSuccess {
loadChapterList(book, changeDruChapterIndex)
}
if (book.isLocalBook()) {
loadChapterList(book, changeDruChapterIndex)
} else {
ReadBook.webBook?.getBookInfo(book, this)
?.onSuccess {
loadChapterList(book, changeDruChapterIndex)
}
}
}
}
private fun loadChapterList(
fun loadChapterList(
book: Book,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
) {
execute {
ReadBook.webBook?.getChapterList(book, this)
?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) {
if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray())
ReadBook.chapterSize = cList.size
ReadBook.loadContent()
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)
?.onSuccess(IO) { cList ->
if (!cList.isNullOrEmpty()) {
if (changeDruChapterIndex == null) {
App.db.bookChapterDao().insert(*cList.toTypedArray())
App.db.bookDao().update(book)
ReadBook.chapterSize = cList.size
ReadBook.loadContent()
} else {
changeDruChapterIndex(cList)
}
} else {
changeDruChapterIndex(cList)
toast(R.string.error_load_toc)
}
} else {
}?.onError {
toast(R.string.error_load_toc)
}
}?.onError {
toast(R.string.error_load_toc)
} ?: autoChangeSource()
} ?: autoChangeSource()
}
}
}
fun changeTo(book1: Book) {
execute {
ReadBook.book?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null
@ -170,7 +185,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
fun removeFromBookshelf(success: (() -> Unit)?) {
execute {
ReadBook.book?.let {
App.db.bookDao().delete(it.bookUrl)
App.db.bookDao().delete(it)
}
}.onSuccess {
success?.invoke()

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

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

@ -8,7 +8,7 @@ import android.view.ViewGroup
import android.widget.SeekBar
import androidx.fragment.app.DialogFragment
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.ui.book.read.Help
import io.legado.app.utils.postEvent
@ -26,8 +26,6 @@ class PaddingConfigDialog : DialogFragment() {
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes
attr.dimAmount = 0.0f
it.attributes = attr
@ -63,35 +61,35 @@ class PaddingConfigDialog : DialogFragment() {
private fun initView() = with(ReadBookConfig.getConfig()) {
iv_padding_top_add.onClick {
seek_padding_top.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_top_remove.onClick {
seek_padding_top.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_bottom_add.onClick {
seek_padding_bottom.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_bottom_remove.onClick {
seek_padding_bottom.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_left_add.onClick {
seek_padding_left.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_left_remove.onClick {
seek_padding_left.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_right_add.onClick {
seek_padding_right.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_padding_right_remove.onClick {
seek_padding_right.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
seek_padding_top.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -103,7 +101,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
seek_padding_bottom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -115,7 +113,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
seek_padding_left.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -127,7 +125,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
seek_padding_right.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -139,7 +137,7 @@ class PaddingConfigDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
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.PreferenceFragmentCompat
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.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent
class ReadAloudConfigDialog : DialogFragment() {
@ -58,17 +59,19 @@ class ReadAloudConfigDialog : DialogFragment() {
}
class ReadAloudPreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
Preference.OnPreferenceChangeListener {
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_aloud)
upPreferenceSummary(
findPreference<ListPreference>(PreferKey.ttsSpeechPer),
AppConfig.ttsSpeechPer
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ATH.applyEdgeEffectColor(listView)
bindPreferenceSummaryToValue(findPreference("ttsSpeechPer"))
}
override fun onResume() {
@ -86,42 +89,35 @@ class ReadAloudConfigDialog : DialogFragment() {
key: String?
) {
when (key) {
"readAloudByPage" -> {
PreferKey.readAloudByPage -> {
if (BaseReadAloudService.isRun) {
postEvent(Bus.MEDIA_BUTTON, false)
postEvent(EventBus.MEDIA_BUTTON, false)
}
}
"readAloudOnLine" -> {
if (BaseReadAloudService.isRun) {
ReadAloud.stop(requireContext())
ReadAloud.aloudClass = ReadAloud.getReadAloudClass()
}
PreferKey.readAloudOnLine -> {
ReadAloud.stop(requireContext())
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 {
val stringValue = newValue.toString()
if (preference is ListPreference) {
val index = preference.findIndexOfValue(stringValue)
// Set the summary to reflect the new value.
preference.setSummary(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
}
private fun bindPreferenceSummaryToValue(preference: Preference?) {
preference?.apply {
onPreferenceChangeListener = this@ReadAloudPreferenceFragment
onPreferenceChange(
this,
context.getPrefString(key)
)
private fun upPreferenceSummary(preference: Preference?, value: String) {
when (preference) {
is ListPreference -> {
val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value.
preference.summary = if (index >= 0) preference.entries[index] else null
}
else -> {
preference?.summary = value
}
}
}

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

@ -10,7 +10,7 @@ import android.widget.SeekBar
import androidx.core.view.get
import androidx.fragment.app.DialogFragment
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.ImageLoader
import io.legado.app.help.ReadBookConfig
@ -37,8 +37,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.decorView.setPadding(0, 0, 0, 0)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 5, 0, 0)
val attr = it.attributes
attr.dimAmount = 0.0f
attr.gravity = Gravity.BOTTOM
@ -83,7 +83,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
textBold = !textBold
tv_text_bold.isSelected = textBold
}
postEvent(Bus.UP_CONFIG, false)
postEvent(EventBus.UP_CONFIG, false)
}
tv_text_font.onClick {
FontSelectDialog().show(childFragmentManager, "fontSelectDialog")
@ -94,7 +94,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
items = resources.getStringArray(R.array.indent).toList()
) { _, index ->
putPrefInt("textIndent", index)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
}
tv_padding.onClick {
@ -112,16 +112,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
iv_text_size_add.onClick {
seek_text_size.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_text_size_remove.onClick {
seek_text_size.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
seek_text_letter_spacing.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
@ -134,16 +134,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
iv_text_letter_spacing_add.onClick {
seek_text_letter_spacing.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_text_letter_spacing_remove.onClick {
seek_text_letter_spacing.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
seek_line_size.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
@ -156,16 +156,16 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
})
iv_line_size_add.onClick {
seek_line_size.progressAdd(1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
iv_line_size_remove.onClick {
seek_line_size.progressAdd(-1)
postEvent(Bus.UP_CONFIG, true)
postEvent(EventBus.UP_CONFIG, true)
}
rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) {
@ -197,7 +197,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
ReadBookConfig.upBg()
upStyle()
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) {
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 io.legado.app.R
import io.legado.app.constant.Bus
import io.legado.app.constant.EventBus
import io.legado.app.utils.postEvent
class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback {
@ -15,7 +15,7 @@ class ContentSelectActionCallback(private val textView: TextView) : ActionMode.C
when (item?.itemId) {
R.id.menu_replace -> {
val text = textView.text.substring(textView.selectionStart, textView.selectionEnd)
postEvent(Bus.REPLACE, text)
postEvent(EventBus.REPLACE, text)
mode?.finish()
return true
}

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

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

@ -67,13 +67,7 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
}
}
}
searchBook.coverUrl.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author)
onClick {
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.primaryTextColor
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.filechooser.FileChooserDialog
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 org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.startService
import org.jetbrains.anko.toast
import java.io.FileNotFoundException
@ -81,21 +80,18 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
when (item.itemId) {
R.id.menu_add_book_source -> startActivity<BookSourceEditActivity>()
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_group_manage -> GroupManageDialog().show(
supportFragmentManager,
"groupManage"
)
R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_import_source_local -> selectFileSys()
R.id.menu_select_all -> adapter.selectAll()
R.id.menu_revert_selection -> adapter.revertSelection()
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds())
R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelectionIds())
R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelectionIds())
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds())
R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelectionIds())
R.id.menu_check_source ->
startService<CheckSourceService>(Pair("data", adapter.getSelectionIds()))
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection())
R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection())
R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection())
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelection())
R.id.menu_check_source -> CheckSource.start(this, adapter.getSelection())
R.id.menu_import_source_onLine -> showImportDialog()
}
if (item.groupId == R.id.source_group) {

@ -1,8 +1,10 @@
package io.legado.app.ui.book.source.manage
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.widget.PopupMenu
import androidx.core.os.bundleOf
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
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),
OnItemTouchCallbackListener {
private val selectedIds = linkedSetOf<String>()
private val selected = linkedSetOf<BookSource>()
fun selectAll() {
getItems().forEach {
selectedIds.add(it.bookSourceUrl)
selected.add(it)
}
notifyItemRangeChanged(0, itemCount, 1)
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
}
fun revertSelection() {
getItems().forEach {
if (selectedIds.contains(it.bookSourceUrl)) {
selectedIds.remove(it.bookSourceUrl)
if (selected.contains(it)) {
selected.remove(it)
} else {
selectedIds.add(it.bookSourceUrl)
selected.add(it)
}
}
notifyItemRangeChanged(0, itemCount, 1)
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
}
fun getSelectionIds(): LinkedHashSet<String> {
val selection = linkedSetOf<String>()
fun getSelection(): LinkedHashSet<BookSource> {
val selection = linkedSetOf<BookSource>()
getItems().map {
if (selectedIds.contains(it.bookSourceUrl)) {
selection.add(it.bookSourceUrl)
if (selected.contains(it)) {
selection.add(it)
}
}
return selection
@ -68,7 +70,8 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) {
if (payloads.isEmpty()) {
val payload = payloads.getOrNull(0) as? Bundle
if (payload == null) {
this.setBackgroundColor(context.backgroundColor)
if (item.bookSourceGroup.isNullOrEmpty()) {
cb_book_source.text = item.bookSourceName
@ -81,12 +84,12 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
item.enabled = swt_enabled.isChecked
callBack.update(item)
}
cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl)
cb_book_source.isChecked = selected.contains(item)
cb_book_source.setOnClickListener {
if (cb_book_source.isChecked) {
selectedIds.add(item.bookSourceUrl)
selected.add(item)
} else {
selectedIds.remove(item.bookSourceUrl)
selected.remove(item)
}
}
iv_edit.onClick { callBack.edit(item) }
@ -104,9 +107,17 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
popupMenu.show()
}
} else {
when (payloads[0]) {
1 -> cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl)
2 -> swt_enabled.isChecked = item.enabled
payload.keySet().map {
when (it) {
"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 {
ids.forEach {
App.db.bookSourceDao().enableSection(it)
sources.forEach {
it.enabled = true
}
App.db.bookSourceDao().update(*sources.toTypedArray())
}
}
fun disableSelection(ids: LinkedHashSet<String>) {
fun disableSelection(sources: LinkedHashSet<BookSource>) {
execute {
ids.forEach {
App.db.bookSourceDao().disableSection(it)
sources.forEach {
it.enabled = false
}
App.db.bookSourceDao().update(*sources.toTypedArray())
}
}
fun enableSelectExplore(ids: LinkedHashSet<String>) {
fun enableSelectExplore(sources: LinkedHashSet<BookSource>) {
execute {
ids.forEach {
App.db.bookSourceDao().enableSectionExplore(it)
sources.forEach {
it.enabledExplore = true
}
App.db.bookSourceDao().update(*sources.toTypedArray())
}
}
fun disableSelectExplore(ids: LinkedHashSet<String>) {
fun disableSelectExplore(sources: LinkedHashSet<BookSource>) {
execute {
ids.forEach {
App.db.bookSourceDao().disableSectionExplore(it)
sources.forEach {
it.enabledExplore = false
}
App.db.bookSourceDao().update(*sources.toTypedArray())
}
}
fun delSelection(ids: LinkedHashSet<String>) {
fun delSelection(sources: LinkedHashSet<BookSource>) {
execute {
ids.forEach {
App.db.bookSourceDao().delSection(it)
}
App.db.bookSourceDao().delete(*sources.toTypedArray())
}
}
fun exportSelection(ids: LinkedHashSet<String>) {
fun exportSelection(sources: LinkedHashSet<BookSource>) {
execute {
ids.map {
App.db.bookSourceDao().getBookSource(it)
}.let {
val json = GSON.toJson(it)
val file =
FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportBookSource.json")
file.writeText(json)
}
val json = GSON.toJson(sources)
val file =
FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportBookSource.json")
file.writeText(json)
}.onSuccess {
context.toast("成功导出至\n${Backup.exportPath}")
}.onError {

@ -1,5 +1,6 @@
package io.legado.app.ui.book.source.manage
import android.os.Bundle
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.data.entities.BookSource
@ -25,19 +26,31 @@ class DiffCallBack(
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return oldItem.bookSourceName == newItem.bookSourceName
&& oldItem.bookSourceGroup == newItem.bookSourceGroup
&& oldItem.enabled == newItem.enabled
if (oldItem.bookSourceName != newItem.bookSourceName)
return false
if (oldItem.bookSourceGroup != newItem.bookSourceGroup)
return false
if (oldItem.enabled != newItem.enabled)
return false
return true
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return when {
oldItem.bookSourceName == newItem.bookSourceName
&& oldItem.bookSourceGroup == newItem.bookSourceGroup
&& oldItem.enabled != newItem.enabled -> 2
else -> null
val payload = Bundle()
if (oldItem.bookSourceName != newItem.bookSourceName) {
payload.putString("name", newItem.bookSourceName)
}
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 androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.R
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.dialog_change_source.*
class ChangeCoverDialog : DialogFragment() {
class ChangeCoverDialog : DialogFragment(),
ChangeCoverViewModel.CallBack,
CoverAdapter.CallBack {
companion object {
const val tag = "changeCoverDialog"
@ -32,7 +35,7 @@ class ChangeCoverDialog : DialogFragment() {
private var callBack: CallBack? = null
private lateinit var viewModel: ChangeCoverViewModel
private lateinit var adapter: CoverAdapter
override lateinit var adapter: CoverAdapter
override fun onStart() {
super.onStart()
@ -46,13 +49,17 @@ class ChangeCoverDialog : DialogFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
callBack = activity as? CallBack
viewModel = getViewModel(ChangeCoverViewModel::class.java)
viewModel.callBack = this
return inflater.inflate(R.layout.dialog_change_source, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
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)
arguments?.let { bundle ->
bundle.getString("name")?.let {
@ -63,8 +70,14 @@ class ChangeCoverDialog : DialogFragment() {
}
}
recycler_view.layoutManager = GridLayoutManager(requireContext(), 3)
adapter = CoverAdapter(requireContext())
adapter = CoverAdapter(requireContext(), this)
recycler_view.adapter = adapter
viewModel.initData()
}
override fun changeTo(coverUrl: String) {
callBack?.coverChangeTo(coverUrl)
dismiss()
}
interface CallBack {

@ -1,12 +1,77 @@
package io.legado.app.ui.changecover
import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
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) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
var callBack: CallBack? = null
var name: 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.SimpleRecyclerAdapter
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.ImageLoader
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) {
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) {
with(holder.itemView) {
item.coverUrl?.let {
ImageLoader.load(context, it)
.centerCrop()
.into(iv_cover)
}
iv_cover.load(item.coverUrl, item.name, item.author)
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 lateinit var viewModel: ChangeSourceViewModel
private lateinit var changeSourceAdapter: ChangeSourceAdapter
override lateinit var changeSourceAdapter: ChangeSourceAdapter
override fun onStart() {
super.onStart()
@ -54,13 +54,14 @@ class ChangeSourceDialog : DialogFragment(),
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
callBack = activity as? CallBack
viewModel = getViewModel(ChangeSourceViewModel::class.java)
viewModel.callBack = this
return inflater.inflate(R.layout.dialog_change_source, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callBack = activity as? CallBack
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
refresh_progress_bar.isAutoLoading = it
})
@ -92,7 +93,6 @@ class ChangeSourceDialog : DialogFragment(),
DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
)
recycler_view.adapter = changeSourceAdapter
viewModel.callBack = this
}
private fun initSearchView() {
@ -139,10 +139,6 @@ class ChangeSourceDialog : DialogFragment(),
override val bookUrl: String?
get() = callBack?.oldBook?.bookUrl
override fun adapter(): ChangeSourceAdapter {
return changeSourceAdapter
}
interface CallBack {
val oldBook: Book?
fun changeTo(book: Book)

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

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

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

@ -12,7 +12,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App
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.help.BookHelp
@ -27,7 +27,6 @@ import io.legado.app.utils.*
class OtherConfigFragment : PreferenceFragmentCompat(),
FileChooserDialog.CallBack,
Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeDownloadPath = 25324
@ -40,8 +39,8 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean("process_text", isProcessTextEnabled())
addPreferencesFromResource(R.xml.pref_config_other)
bindPreferenceSummaryToValue(findPreference(PreferKey.downloadPath))
bindPreferenceSummaryToValue(findPreference(PreferKey.threadCount))
upPreferenceSummary(findPreference(PreferKey.downloadPath), BookHelp.downloadPath)
upPreferenceSummary(findPreference(PreferKey.threadCount), AppConfig.threadCount.toString())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -84,48 +83,26 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
when (key) {
PreferKey.downloadPath -> {
BookHelp.upDownloadPath()
findPreference<Preference>(key)?.summary = getPreferenceString(key).toString()
findPreference<Preference>(key)?.summary = BookHelp.downloadPath
}
PreferKey.recordLog -> LogUtils.upLevel()
PreferKey.processText -> sharedPreferences?.let {
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 {
val stringValue = newValue.toString()
when {
preference is ListPreference -> {
val index = preference.findIndexOfValue(stringValue)
private fun upPreferenceSummary(preference: Preference?, value: String) {
when (preference) {
is ListPreference -> {
val index = preference.findIndexOfValue(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 -> {
preference?.summary = value
}
preference?.key == PreferKey.threadCount -> preference.summary =
getString(R.string.threads_num, stringValue)
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,
requestCodeDownloadPath,
mode = FileChooserDialog.DIRECTORY,
initPath = getPreferenceString(PreferKey.downloadPath).toString()
initPath = BookHelp.downloadPath
)
}
.request()
@ -188,6 +165,8 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
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 io.legado.app.App
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.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
@ -104,7 +104,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
.setTitle("切换默认主题")
.setItems(items){
_,which ->
preference.summary = "${items[which]}"
preference.summary = items[which]
putPrefInt("default_theme", which)
when (which) {
0 -> {
@ -180,7 +180,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
}
private fun recreateActivities() {
postEvent(Bus.RECREATE, "")
postEvent(EventBus.RECREATE, "")
Handler().postDelayed({ activity?.recreate() }, 100L)
}

@ -9,7 +9,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
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.service.help.Download
import io.legado.app.utils.applyTint
@ -67,7 +67,7 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) {
}
override fun observeLiveBus() {
observeEvent<Boolean>(Bus.UP_DOWNLOAD) {
observeEvent<Boolean>(EventBus.UP_DOWNLOAD) {
if (it) {
menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp)
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.data.entities.Book
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.ImageLoader
import io.legado.app.utils.gone
import io.legado.app.utils.visible
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 {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
iv_cover.load(item.coverUrl, item.name, item.author)
onClick {
callBack.showBookInfo(item.toBook())
}

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

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

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

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

@ -3,9 +3,8 @@ package io.legado.app.ui.importbook
import android.app.Application
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
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) {
@ -14,18 +13,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
execute {
uriList.forEach { uriStr ->
DocumentFile.fromSingleUri(context, Uri.parse(uriStr))?.let { doc ->
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 = uriStr, name = name, author = author)
App.db.bookDao().insert(book)
}
LocalBook.importFile(doc)
}
}
}.onFinally {

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

@ -3,7 +3,7 @@ package io.legado.app.ui.main
import android.app.Application
import io.legado.app.App
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.help.http.HttpHelper
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 ->
synchronized(this) {
updateList.add(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl)
postEvent(EventBus.UP_BOOK, book.bookUrl)
}
WebBook(bookSource).getChapterList(book)
.timeout(300000)
.onSuccess(IO) {
synchronized(this) {
updateList.remove(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl)
postEvent(EventBus.UP_BOOK, book.bookUrl)
}
it?.let {
App.db.bookDao().update(book)
@ -42,7 +42,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
.onError {
synchronized(this) {
updateList.remove(book.bookUrl)
postEvent(Bus.UP_BOOK, book.bookUrl)
postEvent(EventBus.UP_BOOK, book.bookUrl)
}
it.printStackTrace()
}

@ -1,5 +1,6 @@
package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -13,16 +14,20 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
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.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.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.download.DownloadActivity
import io.legado.app.ui.importbook.ImportBookActivity
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.view_tab_layout.*
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()
.show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>()
R.id.menu_add_url -> {
}
R.id.menu_arrange_bookshelf -> {
}
R.id.menu_add_url -> addBookByUrl()
R.id.menu_arrange_bookshelf -> startActivity<ArrangeBookActivity>()
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 ->
tab.text = bookGroups[position].groupName
}.attach()
observeEvent<Int>(Bus.UP_TABS) {
observeEvent<Int>(EventBus.UP_TABS) {
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?) {
}

@ -2,8 +2,14 @@ package io.legado.app.ui.main.bookshelf
import android.app.Application
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource
import io.legado.app.model.WebBook
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.Dispatchers.IO
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.data.entities.Book
abstract class BooksAdapter(context: Context, layoutId: Int) :
abstract class BaseBooksAdapter(context: Context, layoutId: Int) :
SimpleRecyclerAdapter<Book>(context, layoutId) {
fun notification(bookUrl: String) {

@ -5,7 +5,6 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.help.ImageLoader
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.invisible
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
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>) {
with(holder.itemView) {
@ -21,13 +20,7 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
ATH.applyBackgroundTint(this)
tv_name.text = item.name
bv_author.text = item.author
item.getDisplayCover()?.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
iv_cover.load(item.getDisplayCover(), item.name, item.author)
onClick { callBack.open(item) }
onLongClick {
callBack.openBookInfo(item)

@ -5,7 +5,6 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.help.ImageLoader
import io.legado.app.lib.theme.ATH
import io.legado.app.utils.invisible
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
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>) {
with(holder.itemView) {
@ -23,13 +22,7 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) :
tv_author.text = item.author
tv_read.text = item.durChapterTitle
tv_last.text = item.latestChapterTitle
item.getDisplayCover()?.let {
ImageLoader.load(context, it)//Glide自动识别http://和file://
.placeholder(R.drawable.image_cover_default)
.error(R.drawable.image_cover_default)
.centerCrop()
.into(iv_cover)
}
iv_cover.load(item.getDisplayCover(), item.name, item.author)
onClick { callBack.open(item) }
onLongClick {
callBack.openBookInfo(item)

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

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

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

Loading…
Cancel
Save