Merge remote-tracking branch 'origin/master'

pull/67/head
Administrator 5 years ago
commit 85090349d5
  1. 7
      app/build.gradle
  2. 28
      app/src/main/assets/updateLog.md
  3. 2
      app/src/main/java/io/legado/app/constant/Bus.kt
  4. 3
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  5. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  6. 3
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  7. 19
      app/src/main/java/io/legado/app/help/BookHelp.kt
  8. 65
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  9. 1
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  10. 99
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  11. 18
      app/src/main/java/io/legado/app/model/Debug.kt
  12. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt
  13. 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  14. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  15. 4
      app/src/main/java/io/legado/app/model/rss/RssParser.kt
  16. 25
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  17. 52
      app/src/main/java/io/legado/app/service/DownloadService.kt
  18. 8
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  19. 3
      app/src/main/java/io/legado/app/service/WebService.kt
  20. 15
      app/src/main/java/io/legado/app/ui/about/DonateActivity.kt
  21. 26
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  22. 22
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  23. 2
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  24. 6
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  25. 954
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlMesh.java
  26. 191
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlPage.kt
  27. 221
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlRenderer.kt
  28. 769
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlView.kt
  29. 6
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  30. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  31. 6
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  32. 558
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  33. 2
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  34. 6
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  35. 2
      app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt
  36. 97
      app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt
  37. 46
      app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt
  38. 20
      app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt
  39. 93
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  40. 27
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  41. 3
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  42. 8
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  43. 2
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  44. 40
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  45. 5
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  46. 16
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  47. 19
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  48. 5
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  49. 229
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  50. 52
      app/src/main/java/io/legado/app/utils/DocumentUtils.kt
  51. BIN
      app/src/main/res/drawable/bg_shadow_bottom.png
  52. 8
      app/src/main/res/drawable/bg_shadow_bottom.xml
  53. BIN
      app/src/main/res/drawable/bg_shadow_bottom_night.png
  54. 8
      app/src/main/res/drawable/bg_shadow_bottom_night.xml
  55. BIN
      app/src/main/res/drawable/bg_shadow_top.png
  56. 8
      app/src/main/res/drawable/bg_shadow_top.xml
  57. BIN
      app/src/main/res/drawable/bg_shadow_top_night.png
  58. 8
      app/src/main/res/drawable/bg_shadow_top_night.xml
  59. 9
      app/src/main/res/drawable/side_nav_bar.xml
  60. 16
      app/src/main/res/layout/dialog_font_select.xml
  61. 18
      app/src/main/res/layout/item_download.xml
  62. 6
      app/src/main/res/layout/item_find_book.xml
  63. 11
      app/src/main/res/menu/download.xml
  64. 15
      app/src/main/res/menu/font_select.xml
  65. 4
      app/src/main/res/menu/main_bookshelf.xml
  66. 6
      app/src/main/res/menu/rss_read.xml
  67. 8
      app/src/main/res/values-night/colors.xml
  68. 6
      app/src/main/res/values/colors.xml
  69. 5
      app/src/main/res/values/strings.xml
  70. 6
      app/src/main/res/xml/pref_config_web_dav.xml

@ -37,6 +37,7 @@ android {
versionName version versionName version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", name + "_" + version) project.ext.set("archivesBaseName", name + "_" + version)
multiDexEnabled true
} }
buildTypes { buildTypes {
release { release {
@ -98,7 +99,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.material:material:1.2.0-alpha02' implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
//lifecycle //lifecycle
@ -107,12 +108,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.2' def room_version = '2.2.3'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
//paging //paging
implementation 'androidx.paging:paging-runtime:2.1.0' implementation 'androidx.paging:paging-runtime:2.1.1'
//anko //anko
def anko_version = '0.10.8' def anko_version = '0.10.8'

@ -4,8 +4,36 @@
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】,提示存储权限,选择允许即可导入成功。 * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】,提示存储权限,选择允许即可导入成功。
* 注意:由于安卓10更改了权限策略,还需要给「允许安装其他应用」的权限才能导入源。MIUI11也需要此权限。 * 注意:由于安卓10更改了权限策略,还需要给「允许安装其他应用」的权限才能导入源。MIUI11也需要此权限。
**2020/01/03**
* 适配Android 10 权限
* 导入旧版本配置不在需要存储权限
* 选择字体不在需要存储权限
**2019/12/30**
* 修改书源调试
- 调试搜索>>输入关键字,如:`系统`
- 调试发现>>输入发现URL,如:`月票榜::https://www.qidian.com/rank/yuepiao?page={{page}}`
- 调试详情页>>输入详情页URL,如:`https://m.qidian.com/book/1015609210`
- 调试目录页>>输入目录页URL,如:`++https://www.zhaishuyuan.com/read/30394`
- 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996`
* 修改订阅中自动添加style的情景
订阅源的内容规则中存在`<style>``style=`,
```
<style>
img{max-width:100% !important; width:auto; height:auto;}
video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
</style>
```
**2019/12/28**
* 添加下载界面
* 添加分组备份
**2019/12/23** **2019/12/23**
* 修复每次打开翻页模式恢复默认的bug * 修复每次打开翻页模式恢复默认的bug
* 修复m3u8报错问题
**2019/12/22** **2019/12/22**
* 更新音频播放界面 * 更新音频播放界面

@ -18,4 +18,6 @@ object Bus {
const val AUDIO_SIZE = "audioSize" const val AUDIO_SIZE = "audioSize"
const val AUDIO_SPEED = "audioSpeed" const val AUDIO_SPEED = "audioSpeed"
const val SHOW_RSS = "showRss" const val SHOW_RSS = "showRss"
const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload"
} }

@ -15,4 +15,7 @@ object PreferKey {
const val cleanCache = "cleanCache" const val cleanCache = "cleanCache"
const val lastGroup = "lastGroup" const val lastGroup = "lastGroup"
const val pageAnim = "pageAnim" const val pageAnim = "pageAnim"
const val readBookFont = "readBookFont"
const val fontFolder = "fontFolder"
const val backupPath = "backupUri"
} }

@ -17,6 +17,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
fun observeLocal(): LiveData<List<Book>> fun observeLocal(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc")
fun observeDownload(): LiveData<List<Book>>
@Query("SELECT * FROM books WHERE `group` = :group") @Query("SELECT * FROM books WHERE `group` = :group")
fun observeByGroup(group: Int): LiveData<List<Book>> fun observeByGroup(group: Int): LiveData<List<Book>>

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

@ -35,13 +35,24 @@ object BookHelp {
FileHelp.getFolder(getBookCachePath()) FileHelp.getFolder(getBookCachePath())
} }
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) { fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return if (content.isEmpty()) return
FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
it.delete()
return@forEach
}
}
val filePath = getChapterPath(book, bookChapter) val filePath = getChapterPath(book, bookChapter)
val file = FileHelp.getFile(filePath) val file = FileHelp.getFile(filePath)
file.writeText(content) file.writeText(content)
} }
fun getChapterCount(book: Book): Int {
return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0
}
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter) val filePath = getChapterPath(book, bookChapter)
runCatching { runCatching {
@ -74,11 +85,15 @@ object BookHelp {
} }
} }
private fun getChapterPath(book: Book, bookChapter: BookChapter): String { private fun getBookFolder(book: Book): String {
val bookFolder = formatFolderName(book.name + book.bookUrl) val bookFolder = formatFolderName(book.name + book.bookUrl)
return "${getBookCachePath()}${File.separator}$bookFolder"
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
val chapterFile = val chapterFile =
String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title)) String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title))
return "${getBookCachePath()}${File.separator}$bookFolder${File.separator}$chapterFile.nb" return "${getBookFolder(book)}${File.separator}$chapterFile.nb"
} }
private fun formatFolderName(folderName: String): String { private fun formatFolderName(folderName: String): String {

@ -1,9 +1,13 @@
package io.legado.app.help.storage package io.legado.app.help.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.FileHelp import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import org.jetbrains.anko.defaultSharedPreferences import org.jetbrains.anko.defaultSharedPreferences
@ -27,8 +31,52 @@ object Backup {
legadoPath + File.separator + "Export" legadoPath + File.separator + "Export"
} }
private fun pbackup(path :String = legadoPath){ fun backup(context: Context, uri: Uri) {
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
when (doc.name) {
"bookshelf.json" -> App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
DocumentUtils.writeText(context, json, doc.uri)
}
}
"bookGroup.json" -> App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
DocumentUtils.writeText(context, json, doc.uri)
}
}
"bookSource.json" -> App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
DocumentUtils.writeText(context, json, doc.uri)
}
}
"rssSource.json" -> App.db.rssSourceDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
DocumentUtils.writeText(context, json, doc.uri)
}
}
"replaceRule.json" -> App.db.replaceRuleDao().all.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
DocumentUtils.writeText(context, json, doc.uri)
}
}
ReadBookConfig.readConfigFileName -> GSON.toJson(ReadBookConfig.configList)?.let {
DocumentUtils.writeText(context, it, doc.uri)
}
"config.xml" -> {
}
}
}
}
private fun pBackup(path: String = legadoPath) {
backupBookshelf(path) backupBookshelf(path)
backupBookGroup(path)
backupBookSource(path) backupBookSource(path)
backupRssSource(path) backupRssSource(path)
backupReplaceRule(path) backupReplaceRule(path)
@ -39,7 +87,7 @@ object Backup {
fun backup() { fun backup() {
doAsync { doAsync {
pbackup() pBackup()
uiThread { uiThread {
App.INSTANCE.toast(R.string.backup_success) App.INSTANCE.toast(R.string.backup_success)
} }
@ -48,7 +96,7 @@ object Backup {
fun autoBackup() { fun autoBackup() {
doAsync { doAsync {
pbackup() pBackup()
} }
} }
@ -56,13 +104,22 @@ object Backup {
App.db.bookDao().allBooks.let { App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookshelf.json") val file = FileHelp.getFile(path + File.separator + "bookshelf.json")
file.writeText(json) file.writeText(json)
} }
} }
} }
private fun backupBookGroup(path: String) {
App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
val file = FileHelp.getFile(path + File.separator + "bookGroup.json")
file.writeText(json)
}
}
}
private fun backupBookSource(path: String) { private fun backupBookSource(path: String) {
App.db.bookSourceDao().all.let { App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {

@ -205,4 +205,5 @@ object OldRule {
return GSON.toJson(map) return GSON.toJson(map)
} }
} }

@ -9,10 +9,7 @@ import com.jayway.jsonpath.ParseContext
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.entities.Book import io.legado.app.data.entities.*
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.entities.RssSource
import io.legado.app.help.FileHelp import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.* import io.legado.app.utils.*
@ -47,6 +44,15 @@ object Restore {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
try {
val file = FileHelp.getFile(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()
}
try { try {
val file = FileHelp.getFile(path + File.separator + "bookSource.json") val file = FileHelp.getFile(path + File.separator + "bookSource.json")
val json = file.readText() val json = file.readText()
@ -105,9 +111,52 @@ object Restore {
fun importYueDuData(context: Context) { fun importYueDuData(context: Context) {
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
try {// 导入书架 try {// 导入书架
val shelfFile = FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json") val shelfFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json")
val json = shelfFile.readText()
val importCount = importOldBookshelf(json)
withContext(Main) {
context.toast("成功导入书籍${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
}
try {// Book source
val sourceFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json")
val json = sourceFile.readText()
val importCount = importOldSource(json)
withContext(Main) {
context.toast("成功导入书源${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入源失败\n${e.localizedMessage}")
}
}
try {// Replace rules
val ruleFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json")
val json = ruleFile.readText()
val importCount = importOldReplaceRule(json)
withContext(Main) {
context.toast("成功导入替换规则${importCount}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
fun importOldBookshelf(json: String): Int {
val books = mutableListOf<Book>() val books = mutableListOf<Book>()
val items: List<Map<String, Any>> = jsonPath.parse(shelfFile.readText()).read("$") val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
val existingBooks = App.db.bookDao().allBookUrls.toSet() val existingBooks = App.db.bookDao().allBookUrls.toSet()
for (item in items) { for (item in items) {
val jsonItem = jsonPath.parse(item) val jsonItem = jsonPath.parse(item)
@ -144,20 +193,12 @@ object Restore {
books.add(book) books.add(book)
} }
App.db.bookDao().insert(*books.toTypedArray()) App.db.bookDao().insert(*books.toTypedArray())
withContext(Main) { return books.size
context.toast("成功导入书籍${books.size}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入书籍失败\n${e.localizedMessage}")
}
} }
try {// Book source fun importOldSource(json: String): Int {
val sourceFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json")
val bookSources = mutableListOf<BookSource>() val bookSources = mutableListOf<BookSource>()
val items: List<Map<String, Any>> = jsonPath.parse(sourceFile.readText()).read("$") val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
for (item in items) { for (item in items) {
val jsonItem = jsonPath.parse(item) val jsonItem = jsonPath.parse(item)
OldRule.jsonToBookSource(jsonItem.jsonString())?.let { OldRule.jsonToBookSource(jsonItem.jsonString())?.let {
@ -165,20 +206,12 @@ object Restore {
} }
} }
App.db.bookSourceDao().insert(*bookSources.toTypedArray()) App.db.bookSourceDao().insert(*bookSources.toTypedArray())
withContext(Main) { return bookSources.size
context.toast("成功导入书源${bookSources.size}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入源失败\n${e.localizedMessage}")
}
} }
try {// Replace rules fun importOldReplaceRule(json: String): Int {
val ruleFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json")
val replaceRules = mutableListOf<ReplaceRule>() val replaceRules = mutableListOf<ReplaceRule>()
val items: List<Map<String, Any>> = jsonPath.parse(ruleFile.readText()).read("$") val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
val existingRules = App.db.replaceRuleDao().all.map { it.pattern }.toSet() val existingRules = App.db.replaceRuleDao().all.map { it.pattern }.toSet()
for ((index: Int, item: Map<String, Any>) in items.withIndex()) { for ((index: Int, item: Map<String, Any>) in items.withIndex()) {
val jsonItem = jsonPath.parse(item) val jsonItem = jsonPath.parse(item)
@ -195,14 +228,6 @@ object Restore {
replaceRules.add(rule) replaceRules.add(rule)
} }
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) App.db.replaceRuleDao().insert(*replaceRules.toTypedArray())
withContext(Main) { return replaceRules.size
context.toast("成功导入替换规则${replaceRules.size}")
}
} catch (e: Exception) {
withContext(Main) {
context.toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
} }
} }

@ -111,6 +111,24 @@ object Debug {
log(webBook.sourceUrl, "⇒开始访问发现页:$url") log(webBook.sourceUrl, "⇒开始访问发现页:$url")
exploreDebug(webBook, url) exploreDebug(webBook, url)
} }
key.startsWith("++")-> {
val url = key.substring(2)
val book = Book()
book.origin = webBook.sourceUrl
book.tocUrl = url
log(webBook.sourceUrl, "⇒开始访目录页:$url")
tocDebug(webBook, book)
}
key.startsWith("--")-> {
val url = key.substring(2)
val book = Book()
book.origin = webBook.sourceUrl
log(webBook.sourceUrl, "⇒开始访正文页:$url")
val chapter = BookChapter()
chapter.title = "调试"
chapter.url = url
contentDebug(webBook, book, chapter, null)
}
else -> { else -> {
log(webBook.sourceUrl, "⇒开始搜索关键字:$key") log(webBook.sourceUrl, "⇒开始搜索关键字:$key")
searchDebug(webBook, key) searchDebug(webBook, key)

@ -16,7 +16,7 @@ object AnalyzeByRegex {
// 新建容器 // 新建容器
val info = arrayListOf<String>() val info = arrayListOf<String>()
for (groupIndex in 0..resM.groupCount()) { for (groupIndex in 0..resM.groupCount()) {
info.add(resM.group(groupIndex)) info.add(resM.group(groupIndex)!!)
} }
info info
} else { } else {
@ -43,7 +43,7 @@ object AnalyzeByRegex {
// 新建容器 // 新建容器
val info = arrayListOf<String>() val info = arrayListOf<String>()
for (groupIndex in 0..resM.groupCount()) { for (groupIndex in 0..resM.groupCount()) {
info.add(resM.group(groupIndex)) info.add(resM.group(groupIndex)!!)
} }
books.add(info) books.add(info)
} while (resM.find()) } while (resM.find())

@ -320,7 +320,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
val pattern = Pattern.compile(rule.replaceRegex) val pattern = Pattern.compile(rule.replaceRegex)
val matcher = pattern.matcher(vResult) val matcher = pattern.matcher(vResult)
if (matcher.find()) { if (matcher.find()) {
matcher.group(0).replaceFirst(rule.replaceRegex.toRegex(), rule.replacement) matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
} else { } else {
"" ""
} }
@ -614,7 +614,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
val pattern = Pattern.compile("(第)(.+?)(章)") val pattern = Pattern.compile("(第)(.+?)(章)")
val matcher = pattern.matcher(s) val matcher = pattern.matcher(s)
return if (matcher.find()) { return if (matcher.find()) {
matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3) matcher.group(1)!! + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3)
} else { } else {
s s
} }

@ -117,7 +117,7 @@ class AnalyzeUrl(
page?.let { page?.let {
val matcher = pagePattern.matcher(ruleUrl) val matcher = pagePattern.matcher(ruleUrl)
while (matcher.find()) { while (matcher.find()) {
val pages = matcher.group(1).split(",") val pages = matcher.group(1)!!.split(",")
ruleUrl = if (page <= pages.size) { ruleUrl = if (page <= pages.size) {
ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' }) ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' })
} else { } else {

@ -122,9 +122,9 @@ object RssParser {
if (matcherImg.find()) { if (matcherImg.find()) {
val imgTag = matcherImg.group(1) val imgTag = matcherImg.group(1)
val patternLink = "src\\s*=\\s*\"(.+?)\"".toPattern() val patternLink = "src\\s*=\\s*\"(.+?)\"".toPattern()
val matcherLink = patternLink.matcher(imgTag) val matcherLink = patternLink.matcher(imgTag!!)
if (matcherLink.find()) { if (matcherLink.find()) {
url = matcherLink.group(1).trim() url = matcherLink.group(1)!!.trim()
} }
} }
return url return url

@ -26,6 +26,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.audio.AudioPlayActivity
@ -121,8 +122,10 @@ class AudioPlayService : BaseService(),
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(Bus.AUDIO_STATE, Status.PLAY) postEvent(Bus.AUDIO_STATE, Status.PLAY)
mediaPlayer.reset() mediaPlayer.reset()
val uri = Uri.parse(url) val analyzeUrl =
mediaPlayer.setDataSource(this, uri, AudioPlay.headers()) AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true)
val uri = Uri.parse(analyzeUrl.url)
mediaPlayer.setDataSource(this, uri, analyzeUrl.headerMap)
mediaPlayer.prepareAsync() mediaPlayer.prepareAsync()
} catch (e: Exception) { } catch (e: Exception) {
launch { launch {
@ -134,6 +137,9 @@ class AudioPlayService : BaseService(),
} }
private fun pause(pause: Boolean) { private fun pause(pause: Boolean) {
if (url.contains(".m3u8", false)) {
stopSelf()
} else {
AudioPlayService.pause = pause AudioPlayService.pause = pause
handler.removeCallbacks(mpRunnable) handler.removeCallbacks(mpRunnable)
position = mediaPlayer.currentPosition position = mediaPlayer.currentPosition
@ -143,6 +149,7 @@ class AudioPlayService : BaseService(),
postEvent(Bus.AUDIO_STATE, Status.PAUSE) postEvent(Bus.AUDIO_STATE, Status.PAUSE)
upNotification() upNotification()
} }
}
private fun resume() { private fun resume() {
pause = false pause = false
@ -182,26 +189,24 @@ class AudioPlayService : BaseService(),
*/ */
override fun onPrepared(mp: MediaPlayer?) { override fun onPrepared(mp: MediaPlayer?) {
if (pause) return if (pause) return
mp?.let { mediaPlayer.start()
mp.start() mediaPlayer.seekTo(position)
mp.seekTo(position) postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration)
postEvent(Bus.AUDIO_SIZE, mp.duration)
bookChapter?.let { bookChapter?.let {
it.end = mp.duration.toLong() it.end = mediaPlayer.duration.toLong()
} }
handler.removeCallbacks(mpRunnable) handler.removeCallbacks(mpRunnable)
handler.post(mpRunnable) handler.post(mpRunnable)
} }
}
/** /**
* 播放出错 * 播放出错
*/ */
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
if (!mediaPlayer.isPlaying) {
AudioPlay.status = Status.STOP AudioPlay.status = Status.STOP
postEvent(Bus.AUDIO_STATE, Status.STOP) postEvent(Bus.AUDIO_STATE, Status.STOP)
launch { launch { toast("error: $what $extra $url") }
toast("error: $what $extra $url")
} }
return true return true
} }

@ -1,16 +1,19 @@
package io.legado.app.service package io.legado.app.service
import android.content.Intent import android.content.Intent
import android.os.Handler
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.Action
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -18,10 +21,26 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() { class DownloadService : BaseService() {
private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf() private var tasks: ArrayList<Coroutine<*>> = arrayListOf()
private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() }
private var notificationContent = "正在启动下载"
private val notificationBuilder by lazy {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
.setOngoing(true)
.setContentTitle(getString(R.string.download_offline))
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>(this, Action.stop)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
updateNotification("正在启动下载") updateNotification(notificationContent)
handler.postDelayed(runnable, 1000)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -32,7 +51,7 @@ class DownloadService : BaseService() {
intent.getIntExtra("start", 0), intent.getIntExtra("start", 0),
intent.getIntExtra("end", 0) intent.getIntExtra("end", 0)
) )
Action.stop -> stopSelf() Action.stop -> stopDownload()
} }
} }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
@ -41,7 +60,9 @@ class DownloadService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
tasks.clear() tasks.clear()
searchPool.close() searchPool.close()
handler.removeCallbacks(runnable)
super.onDestroy() super.onDestroy()
postEvent(Bus.UP_DOWNLOAD, false)
} }
private fun download(bookUrl: String?, start: Int, end: Int) { private fun download(bookUrl: String?, start: Int, end: Int) {
@ -55,7 +76,7 @@ class DownloadService : BaseService() {
if (!BookHelp.hasContent(book, chapter)) { if (!BookHelp.hasContent(book, chapter)) {
webBook.getContent(book, chapter, scope = this, context = searchPool) webBook.getContent(book, chapter, scope = this, context = searchPool)
.onStart { .onStart {
updateNotification(chapter.title) notificationContent = chapter.title
} }
.onSuccess(IO) { content -> .onSuccess(IO) { content ->
content?.let { content?.let {
@ -75,21 +96,24 @@ class DownloadService : BaseService() {
} }
} }
private fun stopDownload() {
tasks.clear()
stopSelf()
}
private fun upDownload() {
updateNotification(notificationContent)
postEvent(Bus.UP_DOWNLOAD, true)
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, 1000)
}
/** /**
* 更新通知 * 更新通知
*/ */
private fun updateNotification(content: String) { private fun updateNotification(content: String) {
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) val builder = notificationBuilder
.setSmallIcon(R.drawable.ic_download) builder.setContentText(content)
.setOngoing(true)
.setContentTitle(getString(R.string.download_offline))
.setContentText(content)
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<DownloadService>(this, Action.stop)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build() val notification = builder.build()
startForeground(AppConst.notificationIdDownload, notification) startForeground(AppConst.notificationIdDownload, notification)
} }

@ -45,16 +45,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
override fun onInit(status: Int) { override fun onInit(status: Int) {
launch { launch {
if (status == TextToSpeech.SUCCESS) { if (status == TextToSpeech.SUCCESS) {
val result = textToSpeech?.setLanguage(Locale.CHINA) textToSpeech?.language = Locale.CHINA
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
toast(R.string.tts_fix)
IntentHelp.toTTSSetting(this@TTSReadAloudService)
stopSelf()
} else {
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener()) textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsIsSuccess = true ttsIsSuccess = true
play() play()
}
} else { } else {
toast(R.string.tts_init_failed) toast(R.string.tts_init_failed)
} }

@ -8,9 +8,11 @@ import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.Action
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent
import io.legado.app.web.HttpServer import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer import io.legado.app.web.WebSocketServer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -54,6 +56,7 @@ class WebService : BaseService() {
if (webSocketServer?.isAlive == true) { if (webSocketServer?.isAlive == true) {
webSocketServer?.stop() webSocketServer?.stop()
} }
postEvent(Bus.WEB_SERVICE_STOP, true)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

@ -34,10 +34,10 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) {
toast(R.string.copy_complete) toast(R.string.copy_complete)
} }
} }
vw_zfb_hb.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/zfbhbrwm.png") } vw_zfb_hb.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png") }
vw_zfb_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/zfbskrwm.jpg") } vw_zfb_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg") }
vw_wx_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/wxskrwm.jpg") } vw_wx_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg") }
vw_qq_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/qqskrwm.jpg") } vw_qq_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg") }
vw_zfb_hb_ssm.setOnClickListener { getZfbHb(this) } vw_zfb_hb_ssm.setOnClickListener { getZfbHb(this) }
} }
@ -75,9 +75,12 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) {
private fun aliDonate(context: Context) { private fun aliDonate(context: Context) {
try { try {
val qrCode = URLEncoder.encode("tsx06677nwdk3javroq4ef0", "utf-8") val qrCode = URLEncoder.encode(
"https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other",
"utf-8"
)
val aliPayQr = val aliPayQr =
"alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/$qrCode" "alipayqr://platformapi/startapp?saId=10000007&qrcode=$qrCode&_t=${System.currentTimeMillis()}"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr))
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {

@ -16,6 +16,7 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -203,6 +204,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
/**
* 按键事件
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
@ -217,11 +221,27 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
KeyEvent.KEYCODE_SPACE -> { KeyEvent.KEYCODE_SPACE -> {
page_view.moveToNextPage() page_view.moveToNextPage()
return true
}
getPrefInt(PreferKey.prevKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.moveToPrevPage()
return true
}
}
getPrefInt(PreferKey.nextKey) -> {
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
page_view.moveToNextPage()
return true
}
} }
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
/**
* 长按事件
*/
override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_BACK -> { KeyEvent.KEYCODE_BACK -> {
@ -232,6 +252,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
return super.onKeyLongPress(keyCode, event) return super.onKeyLongPress(keyCode, event)
} }
/**
* 松开按键事件
*/
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
@ -257,6 +280,9 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
/**
* 音量键翻页
*/
private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean {
if (!read_menu.isVisible) { if (!read_menu.isVisible) {
if (getPrefBoolean("volumeKeyPage", true)) { if (getPrefBoolean("volumeKeyPage", true)) {

@ -27,7 +27,7 @@ import org.jetbrains.anko.sdk27.listeners.onCheckedChange
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment() { class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -86,17 +86,7 @@ class ReadStyleDialog : DialogFragment() {
postEvent(Bus.UP_CONFIG, false) postEvent(Bus.UP_CONFIG, false)
} }
tv_text_font.onClick { tv_text_font.onClick {
FontSelectDialog(requireContext()).apply { FontSelectDialog().show(childFragmentManager, "fontSelectDialog")
curPath = requireContext().getPrefString("readBookFont")
defaultFont = {
requireContext().putPrefString("readBookFont", "")
postEvent(Bus.UP_CONFIG, true)
}
selectFile = {
requireContext().putPrefString("readBookFont", it)
postEvent(Bus.UP_CONFIG, true)
}
}.show()
} }
tv_text_indent.onClick { tv_text_indent.onClick {
selector( selector(
@ -270,4 +260,12 @@ class ReadStyleDialog : DialogFragment() {
else -> bg0.borderColor = requireContext().accentColor else -> bg0.borderColor = requireContext().accentColor
} }
} }
override val curFontPath: String
get() = requireContext().getPrefString(PreferKey.readBookFont) ?: ""
override fun selectFile(path: String) {
requireContext().putPrefString(PreferKey.readBookFont, path)
postEvent(Bus.UP_CONFIG, true)
}
} }

@ -81,7 +81,7 @@ class ContentView : FrameLayout {
tv_bottom_right.setTextColor(it) tv_bottom_right.setTextColor(it)
} }
} }
context.getPrefString("readBookFont")?.let { context.getPrefString(PreferKey.readBookFont)?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
content_text_view.typeface = Typeface.createFromFile(it) content_text_view.typeface = Typeface.createFromFile(it)
} else { } else {

@ -9,7 +9,6 @@ import android.widget.FrameLayout
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.curl.CurlView
import io.legado.app.ui.book.read.page.delegate.* import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
@ -26,7 +25,6 @@ class PageView(context: Context, attrs: AttributeSet) :
var prevPage: ContentView? = null var prevPage: ContentView? = null
var curPage: ContentView? = null var curPage: ContentView? = null
var nextPage: ContentView? = null var nextPage: ContentView? = null
var curlView: CurlView? = null
init { init {
callBack = activity as? CallBack callBack = activity as? CallBack
@ -91,10 +89,6 @@ class PageView(context: Context, attrs: AttributeSet) :
} }
fun upPageAnim(pageAnim: Int) { fun upPageAnim(pageAnim: Int) {
if (curlView != null) {
removeView(curlView)
curlView = null
}
pageDelegate = null pageDelegate = null
pageDelegate = when (pageAnim) { pageDelegate = when (pageAnim) {
0 -> CoverPageDelegate(this) 0 -> CoverPageDelegate(this)

@ -1,954 +0,0 @@
package io.legado.app.ui.book.read.page.curl;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
/**
* Class implementing actual curl/page rendering.
*
* @author harism
*/
public class CurlMesh {
// Flag for rendering some lines used for developing. Shows
// curl position and one for the direction from the
// position given. Comes handy once playing around with different
// ways for following pointer.
private static final boolean DRAW_CURL_POSITION = false;
// Flag for drawing polygon outlines. Using this flag crashes on emulator
// due to reason unknown to me. Leaving it here anyway as seeing polygon
// outlines gives good insight how original rectangle is divided.
private static final boolean DRAW_POLYGON_OUTLINES = false;
// Flag for enabling shadow rendering.
private static final boolean DRAW_SHADOW = true;
// Flag for texture rendering. While this is likely something you
// don't want to do it's been used for development purposes as texture
// rendering is rather slow on emulator.
private static final boolean DRAW_TEXTURE = true;
// Colors for shadow. Inner one is the color drawn next to surface where
// shadowed area starts and outer one is color shadow ends to.
private static final float[] SHADOW_INNER_COLOR = {0f, 0f, 0f, .5f};
private static final float[] SHADOW_OUTER_COLOR = {0f, 0f, 0f, .0f};
// Let's avoid using 'new' as much as possible. Meaning we introduce arrays
// once here and reuse them on runtime. Doesn't really have very much effect
// but avoids some garbage collections from happening.
private Array<ShadowVertex> mArrDropShadowVertices;
private Array<Vertex> mArrIntersections;
private Array<Vertex> mArrOutputVertices;
private Array<Vertex> mArrRotatedVertices;
private Array<Double> mArrScanLines;
private Array<ShadowVertex> mArrSelfShadowVertices;
private Array<ShadowVertex> mArrTempShadowVertices;
private Array<Vertex> mArrTempVertices;
// Buffers for feeding rasterizer.
private FloatBuffer mBufColors;
private FloatBuffer mBufCurlPositionLines;
private FloatBuffer mBufShadowColors;
private FloatBuffer mBufShadowVertices;
private FloatBuffer mBufTexCoords;
private FloatBuffer mBufVertices;
private int mCurlPositionLinesCount;
private int mDropShadowCount;
// Boolean for 'flipping' texture sideways.
private boolean mFlipTexture = false;
// Maximum number of split lines used for creating a curl.
private int mMaxCurlSplits;
// Bounding rectangle for this mesh. mRectagle[0] = top-left corner,
// mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3]
// bottom-right.
private final Vertex[] mRectangle = new Vertex[4];
private int mSelfShadowCount;
private boolean mTextureBack = false;
// Texture ids and other variables.
private int[] mTextureIds = null;
private final CurlPage mTexturePage = new CurlPage();
private final RectF mTextureRectBack = new RectF();
private final RectF mTextureRectFront = new RectF();
private int mVerticesCountBack;
private int mVerticesCountFront;
/**
* Constructor for mesh object.
*
* @param maxCurlSplits Maximum number curl can be divided into. The bigger the value
* the smoother curl will be. With the cost of having more
* polygons for drawing.
*/
public CurlMesh(int maxCurlSplits) {
// There really is no use for 0 splits.
mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;
mArrScanLines = new Array<>(maxCurlSplits + 2);
mArrOutputVertices = new Array<>(7);
mArrRotatedVertices = new Array<>(4);
mArrIntersections = new Array<>(2);
mArrTempVertices = new Array<>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex());
}
if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
mArrTempShadowVertices.add(new ShadowVertex());
}
}
// Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
// bottom-left, index 2 = top-right and index 3 = bottom-right.
for (int i = 0; i < 4; ++i) {
mRectangle[i] = new Vertex();
}
// Set up shadow penumbra direction to each vertex. We do fake 'self
// shadow' calculations based on this information.
mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;
if (DRAW_CURL_POSITION) {
mCurlPositionLinesCount = 3;
ByteBuffer hvbb = ByteBuffer
.allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
hvbb.order(ByteOrder.nativeOrder());
mBufCurlPositionLines = hvbb.asFloatBuffer();
mBufCurlPositionLines.position(0);
}
// There are 4 vertices from bounding rect, max 2 from adding split line
// to two corners and curl consists of max mMaxCurlSplits lines each
// outputting 2 vertices.
int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
vbb.order(ByteOrder.nativeOrder());
mBufVertices = vbb.asFloatBuffer();
mBufVertices.position(0);
if (DRAW_TEXTURE) {
ByteBuffer tbb = ByteBuffer
.allocateDirect(maxVerticesCount * 2 * 4);
tbb.order(ByteOrder.nativeOrder());
mBufTexCoords = tbb.asFloatBuffer();
mBufTexCoords.position(0);
}
ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
cbb.order(ByteOrder.nativeOrder());
mBufColors = cbb.asFloatBuffer();
mBufColors.position(0);
if (DRAW_SHADOW) {
int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
ByteBuffer scbb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 4 * 4);
scbb.order(ByteOrder.nativeOrder());
mBufShadowColors = scbb.asFloatBuffer();
mBufShadowColors.position(0);
ByteBuffer sibb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 3 * 4);
sibb.order(ByteOrder.nativeOrder());
mBufShadowVertices = sibb.asFloatBuffer();
mBufShadowVertices.position(0);
mDropShadowCount = mSelfShadowCount = 0;
}
}
/**
* Adds vertex to buffers.
*/
private void addVertex(Vertex vertex) {
mBufVertices.put((float) vertex.mPosX);
mBufVertices.put((float) vertex.mPosY);
mBufVertices.put((float) vertex.mPosZ);
mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
mBufColors.put(Color.alpha(vertex.mColor) / 255f);
if (DRAW_TEXTURE) {
mBufTexCoords.put((float) vertex.mTexX);
mBufTexCoords.put((float) vertex.mTexY);
}
}
/**
* Sets curl for this mesh.
*
* @param curlPos Position for curl 'center'. Can be any point on line collinear
* to curl.
* @param curlDir Curl direction, should be normalized.
* @param radius Radius of curl.
*/
public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {
// First add some 'helper' lines used for development.
if (DRAW_CURL_POSITION) {
mBufCurlPositionLines.position(0);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y - 1.0f);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y + 1.0f);
mBufCurlPositionLines.put(curlPos.x - 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);
mBufCurlPositionLines.position(0);
}
// Actual 'curl' implementation starts here.
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Calculate curl angle from direction.
double curlAngle = Math.acos(curlDir.x);
curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;
// Initiate rotated rectangle which's is translated to curlPos and
// rotated so that curl direction heads to right (1,0). Vertices are
// ordered in ascending order based on x -coordinate at the same time.
// And using y -coordinate in very rare case in which two vertices have
// same x -coordinate.
mArrTempVertices.addAll(mArrRotatedVertices);
mArrRotatedVertices.clear();
for (int i = 0; i < 4; ++i) {
Vertex v = mArrTempVertices.remove(0);
v.set(mRectangle[i]);
v.translate(-curlPos.x, -curlPos.y);
v.rotateZ(-curlAngle);
int j = 0;
for (; j < mArrRotatedVertices.size(); ++j) {
Vertex v2 = mArrRotatedVertices.get(j);
if (v.mPosX > v2.mPosX) {
break;
}
if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
break;
}
}
mArrRotatedVertices.add(j, v);
}
// Rotated rectangle lines/vertex indices. We need to find bounding
// lines for rotated rectangle. After sorting vertices according to
// their x -coordinate we don't have to worry about vertices at indices
// 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
// opposing corner from vertex 0. So we are calculating distance from
// vertex 0 to vertices 2 and 3 - and altering line indices if needed.
// Also vertices/lines are given in an order first one has x -coordinate
// at least the latter one. This property is used in getIntersections to
// see if there is an intersection.
int[][] lines = {{0, 1}, {0, 2}, {1, 3}, {2, 3}};
{
// TODO: There really has to be more 'easier' way of doing this -
// not including extensive use of sqrt.
Vertex v0 = mArrRotatedVertices.get(0);
Vertex v2 = mArrRotatedVertices.get(2);
Vertex v3 = mArrRotatedVertices.get(3);
double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
* (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
* (v0.mPosY - v2.mPosY));
double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
* (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
* (v0.mPosY - v3.mPosY));
if (dist2 > dist3) {
lines[1][1] = 3;
lines[2][1] = 2;
}
}
mVerticesCountFront = mVerticesCountBack = 0;
if (DRAW_SHADOW) {
mArrTempShadowVertices.addAll(mArrDropShadowVertices);
mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
mArrDropShadowVertices.clear();
mArrSelfShadowVertices.clear();
}
// Length of 'curl' curve.
double curlLength = Math.PI * radius;
// Calculate scan lines.
// TODO: Revisit this code one day. There is room for optimization here.
mArrScanLines.clear();
if (mMaxCurlSplits > 0) {
mArrScanLines.add((double) 0);
}
for (int i = 1; i < mMaxCurlSplits; ++i) {
mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
}
// As mRotatedVertices is ordered regarding x -coordinate, adding
// this scan line produces scan area picking up vertices which are
// rotated completely. One could say 'until infinity'.
mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);
// Start from right most vertex. Pretty much the same as first scan area
// is starting from 'infinity'.
double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;
for (int i = 0; i < mArrScanLines.size(); ++i) {
// Once we have scanXmin and scanXmax we have a scan area to start
// working with.
double scanXmin = mArrScanLines.get(i);
// First iterate 'original' rectangle vertices within scan area.
for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
Vertex v = mArrRotatedVertices.get(j);
// Test if vertex lies within this scan area.
// TODO: Frankly speaking, can't remember why equality check was
// added to both ends. Guessing it was somehow related to case
// where radius=0f, which, given current implementation, could
// be handled much more effectively anyway.
if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
// Pop out a vertex from temp vertices.
Vertex n = mArrTempVertices.remove(0);
n.set(v);
// This is done solely for triangulation reasons. Given a
// rotated rectangle it has max 2 vertices having
// intersection.
Array<Vertex> intersections = getIntersections(
mArrRotatedVertices, lines, n.mPosX);
// In a sense one could say we're adding vertices always in
// two, positioned at the ends of intersecting line. And for
// triangulation to work properly they are added based on y
// -coordinate. And this if-else is doing it for us.
if (intersections.size() == 1
&& intersections.get(0).mPosY > v.mPosY) {
// In case intersecting vertex is higher add it first.
mArrOutputVertices.addAll(intersections);
mArrOutputVertices.add(n);
} else if (intersections.size() <= 1) {
// Otherwise add original vertex first.
mArrOutputVertices.add(n);
mArrOutputVertices.addAll(intersections);
} else {
// There should never be more than 1 intersecting
// vertex. But if it happens as a fallback simply skip
// everything.
mArrTempVertices.add(n);
mArrTempVertices.addAll(intersections);
}
}
}
// Search for scan line intersections.
Array<Vertex> intersections = getIntersections(mArrRotatedVertices,
lines, scanXmin);
// We expect to get 0 or 2 vertices. In rare cases there's only one
// but in general given a scan line intersecting rectangle there
// should be 2 intersecting vertices.
if (intersections.size() == 2) {
// There were two intersections, add them based on y
// -coordinate, higher first, lower last.
Vertex v1 = intersections.get(0);
Vertex v2 = intersections.get(1);
if (v1.mPosY < v2.mPosY) {
mArrOutputVertices.add(v2);
mArrOutputVertices.add(v1);
} else {
mArrOutputVertices.addAll(intersections);
}
} else if (intersections.size() != 0) {
// This happens in a case in which there is a original vertex
// exactly at scan line or something went very much wrong if
// there are 3+ vertices. What ever the reason just return the
// vertices to temp vertices for later use. In former case it
// was handled already earlier once iterating through
// mRotatedVertices, in latter case it's better to avoid doing
// anything with them.
mArrTempVertices.addAll(intersections);
}
// Add vertices found during this iteration to vertex etc buffers.
while (mArrOutputVertices.size() > 0) {
Vertex v = mArrOutputVertices.remove(0);
mArrTempVertices.add(v);
// Local texture front-facing flag.
boolean textureFront;
// Untouched vertices.
if (i == 0) {
textureFront = true;
mVerticesCountFront++;
}
// 'Completely' rotated vertices.
else if (i == mArrScanLines.size() - 1 || curlLength == 0) {
v.mPosX = -(curlLength + v.mPosX);
v.mPosZ = 2 * radius;
v.mPenumbraX = -v.mPenumbraX;
textureFront = false;
mVerticesCountBack++;
}
// Vertex lies within 'curl'.
else {
// Even though it's not obvious from the if-else clause,
// here v.mPosX is between [-curlLength, 0]. And we can do
// calculations around a half cylinder.
double rotY = Math.PI * (v.mPosX / curlLength);
v.mPosX = radius * Math.sin(rotY);
v.mPosZ = radius - (radius * Math.cos(rotY));
v.mPenumbraX *= Math.cos(rotY);
// Map color multiplier to [.1f, 1f] range.
v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math
.sin(rotY) + 1));
if (v.mPosZ >= radius) {
textureFront = false;
mVerticesCountBack++;
} else {
textureFront = true;
mVerticesCountFront++;
}
}
// We use local textureFront for flipping backside texture
// locally. Plus additionally if mesh is in flip texture mode,
// we'll make the procedure "backwards". Also, until this point,
// texture coordinates are within [0, 1] range so we'll adjust
// them to final texture coordinates too.
if (textureFront != mFlipTexture) {
v.mTexX *= mTextureRectFront.right;
v.mTexY *= mTextureRectFront.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
} else {
v.mTexX *= mTextureRectBack.right;
v.mTexY *= mTextureRectBack.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
}
// Move vertex back to 'world' coordinates.
v.rotateZ(curlAngle);
v.translate(curlPos.x, curlPos.y);
addVertex(v);
// Drop shadow is cast 'behind' the curl.
if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x;
sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y;
sv.mPenumbraColor = v.mPosZ / radius;
int idx = (mArrDropShadowVertices.size() + 1) / 2;
mArrDropShadowVertices.add(idx, sv);
}
// Self shadow is cast partly over mesh.
if (DRAW_SHADOW && v.mPosZ > radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX;
sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY;
sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius);
int idx = (mArrSelfShadowVertices.size() + 1) / 2;
mArrSelfShadowVertices.add(idx, sv);
}
}
// Switch scanXmin as scanXmax for next iteration.
scanXmax = scanXmin;
}
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Add shadow Vertices.
if (DRAW_SHADOW) {
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
mDropShadowCount = 0;
for (int i = 0; i < mArrDropShadowVertices.size(); ++i) {
ShadowVertex sv = mArrDropShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mDropShadowCount += 2;
}
mSelfShadowCount = 0;
for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) {
ShadowVertex sv = mArrSelfShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mSelfShadowCount += 2;
}
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
}
}
/**
* Calculates intersections for given scan line.
*/
private Array<Vertex> getIntersections(Array<Vertex> vertices,
int[][] lineIndices, double scanX) {
mArrIntersections.clear();
// Iterate through rectangle lines each re-presented as a pair of
// vertices.
for (int[] lineIndex : lineIndices) {
Vertex v1 = vertices.get(lineIndex[0]);
Vertex v2 = vertices.get(lineIndex[1]);
// Here we expect that v1.mPosX >= v2.mPosX and wont do intersection
// test the opposite way.
if (v1.mPosX > scanX && v2.mPosX < scanX) {
// There is an intersection, calculate coefficient telling 'how
// far' scanX is from v2.
double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX);
Vertex n = mArrTempVertices.remove(0);
n.set(v2);
n.mPosX = scanX;
n.mPosY += (v1.mPosY - v2.mPosY) * c;
if (DRAW_TEXTURE) {
n.mTexX += (v1.mTexX - v2.mTexX) * c;
n.mTexY += (v1.mTexY - v2.mTexY) * c;
}
if (DRAW_SHADOW) {
n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c;
n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c;
}
mArrIntersections.add(n);
}
}
return mArrIntersections;
}
/**
* Getter for textures page for this mesh.
*/
public synchronized CurlPage getTexturePage() {
return mTexturePage;
}
/**
* Renders our page curl mesh.
*/
public synchronized void onDrawFrame(GL10 gl) {
// First allocate texture if there is not one yet.
if (DRAW_TEXTURE && mTextureIds == null) {
// Generate texture.
mTextureIds = new int[2];
gl.glGenTextures(2, mTextureIds, 0);
for (int textureId : mTextureIds) {
// Set texture attributes.
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);
}
}
if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
Bitmap texture = mTexturePage.getTexture(mTextureRectFront,
CurlPage.SIDE_FRONT);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
mTextureBack = mTexturePage.hasBackTexture();
if (mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
texture = mTexturePage.getTexture(mTextureRectBack,
CurlPage.SIDE_BACK);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
} else {
mTextureRectBack.set(mTextureRectFront);
}
mTexturePage.recycle();
reset();
}
// Some 'global' settings.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// TODO: Drop shadow drawing is done temporarily here to hide some
// problems with its calculation.
if (DRAW_SHADOW) {
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_TEXTURE) {
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords);
}
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
// Enable color array.
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors);
// Draw front facing blank vertices.
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
// Draw front facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (!mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
int backStartIdx = Math.max(0, mVerticesCountFront - 2);
int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx;
// Draw back facing blank vertices.
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
// Draw back facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
// Disable textures and color array.
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
if (DRAW_POLYGON_OUTLINES) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_CURL_POSITION) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f);
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines);
gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_SHADOW) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount,
mSelfShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
/**
* Resets mesh to 'initial' state. Meaning this mesh will draw a plain
* textured rectangle after call to this method.
*/
public synchronized void reset() {
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
for (int i = 0; i < 4; ++i) {
Vertex tmp = mArrTempVertices.get(0);
tmp.set(mRectangle[i]);
if (mFlipTexture) {
tmp.mTexX *= mTextureRectBack.right;
tmp.mTexY *= mTextureRectBack.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
} else {
tmp.mTexX *= mTextureRectFront.right;
tmp.mTexY *= mTextureRectFront.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
}
addVertex(tmp);
}
mVerticesCountFront = 4;
mVerticesCountBack = 0;
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
mDropShadowCount = mSelfShadowCount = 0;
}
/**
* Resets allocated texture id forcing creation of new one. After calling
* this method you most likely want to set bitmap too as it's lost. This
* method should be called only once e.g GL context is re-created as this
* method does not release previous texture id, only makes sure new one is
* requested on next render.
*/
public synchronized void resetTexture() {
mTextureIds = null;
}
/**
* If true, flips texture sideways.
*/
public synchronized void setFlipTexture(boolean flipTexture) {
mFlipTexture = flipTexture;
if (flipTexture) {
setTexCoords(1f, 0f, 0f, 1f);
} else {
setTexCoords(0f, 0f, 1f, 1f);
}
}
/**
* Update mesh bounds.
*/
public void setRect(RectF r) {
mRectangle[0].mPosX = r.left;
mRectangle[0].mPosY = r.top;
mRectangle[1].mPosX = r.left;
mRectangle[1].mPosY = r.bottom;
mRectangle[2].mPosX = r.right;
mRectangle[2].mPosY = r.top;
mRectangle[3].mPosX = r.right;
mRectangle[3].mPosY = r.bottom;
}
/**
* Sets texture coordinates to mRectangle vertices.
*/
private synchronized void setTexCoords(float left, float top, float right,
float bottom) {
mRectangle[0].mTexX = left;
mRectangle[0].mTexY = top;
mRectangle[1].mTexX = left;
mRectangle[1].mTexY = bottom;
mRectangle[2].mTexX = right;
mRectangle[2].mTexY = top;
mRectangle[3].mTexX = right;
mRectangle[3].mTexY = bottom;
}
/**
* Simple fixed size array implementation.
*/
private class Array<T> {
private Object[] mArray;
private int mCapacity;
private int mSize;
Array(int capacity) {
mCapacity = capacity;
mArray = new Object[capacity];
}
public void add(int index, T item) {
if (index < 0 || index > mSize || mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
System.arraycopy(mArray, index, mArray, index + 1, mSize - index);
mArray[index] = item;
++mSize;
}
public void add(T item) {
if (mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
mArray[mSize++] = item;
}
public void addAll(Array<T> array) {
if (mSize + array.size() > mCapacity) {
throw new IndexOutOfBoundsException();
}
for (int i = 0; i < array.size(); ++i) {
mArray[mSize++] = array.get(i);
}
}
public void clear() {
mSize = 0;
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
return (T) mArray[index];
}
@SuppressWarnings("unchecked")
public T remove(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
T item = (T) mArray[index];
if (mSize - 1 - index >= 0)
System.arraycopy(mArray, index + 1, mArray, index, mSize - 1 - index);
--mSize;
return item;
}
public int size() {
return mSize;
}
}
/**
* Holder for shadow vertex information.
*/
private class ShadowVertex {
double mPenumbraColor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
}
/**
* Holder for vertex information.
*/
private class Vertex {
int mColor;
float mColorFactor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
double mTexX;
double mTexY;
Vertex() {
mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
mColorFactor = 1.0f;
}
void rotateZ(double theta) {
double cos = Math.cos(theta);
double sin = Math.sin(theta);
double x = mPosX * cos + mPosY * sin;
double y = mPosX * -sin + mPosY * cos;
mPosX = x;
mPosY = y;
double px = mPenumbraX * cos + mPenumbraY * sin;
double py = mPenumbraX * -sin + mPenumbraY * cos;
mPenumbraX = px;
mPenumbraY = py;
}
public void set(Vertex vertex) {
mPosX = vertex.mPosX;
mPosY = vertex.mPosY;
mPosZ = vertex.mPosZ;
mTexX = vertex.mTexX;
mTexY = vertex.mTexY;
mPenumbraX = vertex.mPenumbraX;
mPenumbraY = vertex.mPenumbraY;
mColor = vertex.mColor;
mColorFactor = vertex.mColorFactor;
}
public void translate(double dx, double dy) {
mPosX += dx;
mPosY += dy;
}
}
}

@ -1,191 +0,0 @@
package io.legado.app.ui.book.read.page.curl
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.RectF
/**
* Storage class for page textures, blend colors and possibly some other values
* in the future.
*
* @author harism
*/
class CurlPage {
private var mColorBack: Int = 0
private var mColorFront: Int = 0
private var mTextureBack: Bitmap? = null
private var mTextureFront: Bitmap? = null
/**
* Returns true if textures have changed.
*/
var texturesChanged: Boolean = false
private set
/**
* Default constructor.
*/
init {
reset()
}
/**
* Getter for color.
*/
fun getColor(side: Int): Int {
return when (side) {
SIDE_FRONT -> mColorFront
else -> mColorBack
}
}
/**
* Calculates the next highest power of two for a given integer.
*/
private fun getNextHighestPO2(n: Int): Int {
var n1 = n
n1 -= 1
n1 = n1 or (n1 shr 1)
n1 = n1 or (n1 shr 2)
n1 = n1 or (n1 shr 4)
n1 = n1 or (n1 shr 8)
n1 = n1 or (n1 shr 16)
n1 = n1 or (n1 shr 32)
return n1 + 1
}
/**
* Generates nearest power of two sized Bitmap for give Bitmap. Returns this
* new Bitmap using default return statement + original texture coordinates
* are stored into RectF.
*/
private fun getTexture(bitmap: Bitmap, textureRect: RectF): Bitmap {
// Bitmap original size.
val w = bitmap.width
val h = bitmap.height
// Bitmap size expanded to next power of two. This is done due to
// the requirement on many devices, texture width and height should
// be power of two.
val newW = getNextHighestPO2(w)
val newH = getNextHighestPO2(h)
// TODO: Is there another way to create a bigger Bitmap and copy
// original Bitmap to it more efficiently? Immutable bitmap anyone?
val bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.config)
val c = Canvas(bitmapTex)
c.drawBitmap(bitmap, 0f, 0f, null)
// Calculate final texture coordinates.
val texX = w.toFloat() / newW
val texY = h.toFloat() / newH
textureRect.set(0f, 0f, texX, texY)
return bitmapTex
}
/**
* Getter for textures. Creates Bitmap sized to nearest power of two, copies
* original Bitmap into it and returns it. RectF given as parameter is
* filled with actual texture coordinates in this new upscaled texture
* Bitmap.
*/
fun getTexture(textureRect: RectF, side: Int): Bitmap {
return when (side) {
SIDE_FRONT -> getTexture(mTextureFront!!, textureRect)
else -> getTexture(mTextureBack!!, textureRect)
}
}
/**
* Returns true if back siding texture exists and it differs from front
* facing one.
*/
fun hasBackTexture(): Boolean {
return mTextureFront != mTextureBack
}
/**
* Recycles and frees underlying Bitmaps.
*/
fun recycle() {
if (mTextureFront != null) {
mTextureFront!!.recycle()
}
mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
mTextureFront!!.eraseColor(mColorFront)
if (mTextureBack != null) {
mTextureBack!!.recycle()
}
mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
mTextureBack!!.eraseColor(mColorBack)
texturesChanged = false
}
/**
* Resets this CurlPage into its initial state.
*/
fun reset() {
mColorBack = Color.WHITE
mColorFront = Color.WHITE
recycle()
}
/**
* Setter blend color.
*/
fun setColor(color: Int, side: Int) {
when (side) {
SIDE_FRONT -> mColorFront = color
SIDE_BACK -> mColorBack = color
else -> {
mColorBack = color
mColorFront = mColorBack
}
}
}
/**
* Setter for textures.
*/
fun setTexture(texture: Bitmap?, side: Int) {
var texture1 = texture
if (texture1 == null) {
texture1 = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
if (side == SIDE_BACK) {
texture1!!.eraseColor(mColorBack)
} else {
texture1!!.eraseColor(mColorFront)
}
}
when (side) {
SIDE_FRONT -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
mTextureFront = texture1
}
SIDE_BACK -> {
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
}
SIDE_BOTH -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
mTextureFront = mTextureBack
}
}
texturesChanged = true
}
companion object {
const val SIDE_BACK = 2
const val SIDE_BOTH = 3
const val SIDE_FRONT = 1
}
}

@ -1,221 +0,0 @@
package io.legado.app.ui.book.read.page.curl
import android.graphics.Color
import android.graphics.PointF
import android.graphics.RectF
import android.opengl.GLSurfaceView
import android.opengl.GLU
import java.util.*
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/**
* Actual renderer class.
*
* @author harism
*/
class CurlRenderer(private val mObserver: Observer) : GLSurfaceView.Renderer {
// Background fill color.
private var mBackgroundColor: Int = 0
// Curl meshes used for static and dynamic rendering.
private val mCurlMeshes: Vector<CurlMesh> = Vector()
private val mMargins = RectF()
// Page rectangles.
private val mPageRectLeft: RectF = RectF()
private val mPageRectRight: RectF = RectF()
// View mode.
private var mViewMode = SHOW_ONE_PAGE
// Screen size.
private var mViewportWidth: Int = 0
private var mViewportHeight: Int = 0
// Rect for render area.
private val mViewRect = RectF()
/**
* Adds CurlMesh to this renderer.
*/
@Synchronized
fun addCurlMesh(mesh: CurlMesh) {
removeCurlMesh(mesh)
mCurlMeshes.add(mesh)
}
/**
* Returns rect reserved for left or right page. Value page should be
* PAGE_LEFT or PAGE_RIGHT.
*/
fun getPageRect(page: Int): RectF? {
if (page == PAGE_LEFT) {
return mPageRectLeft
} else if (page == PAGE_RIGHT) {
return mPageRectRight
}
return null
}
@Synchronized
override fun onDrawFrame(gl: GL10) {
mObserver.onDrawFrame()
gl.glClearColor(
Color.red(mBackgroundColor) / 255f,
Color.green(mBackgroundColor) / 255f,
Color.blue(mBackgroundColor) / 255f,
Color.alpha(mBackgroundColor) / 255f
)
gl.glClear(GL10.GL_COLOR_BUFFER_BIT)
gl.glLoadIdentity()
if (!mObserver.canDraw) {
return
}
for (i in mCurlMeshes.indices) {
mCurlMeshes[i].onDrawFrame(gl)
}
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
gl.glViewport(0, 0, width, height)
mViewportWidth = width
mViewportHeight = height
val ratio = width.toFloat() / height
mViewRect.top = 1.0f
mViewRect.bottom = -1.0f
mViewRect.left = -ratio
mViewRect.right = ratio
updatePageRect()
gl.glMatrixMode(GL10.GL_PROJECTION)
gl.glLoadIdentity()
GLU.gluOrtho2D(
gl, mViewRect.left, mViewRect.right,
mViewRect.bottom, mViewRect.top
)
gl.glMatrixMode(GL10.GL_MODELVIEW)
gl.glLoadIdentity()
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
gl.glClearColor(0f, 0f, 0f, 1f)
gl.glShadeModel(GL10.GL_SMOOTH)
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST)
gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST)
gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST)
gl.glEnable(GL10.GL_LINE_SMOOTH)
gl.glDisable(GL10.GL_DEPTH_TEST)
gl.glDisable(GL10.GL_CULL_FACE)
mObserver.onSurfaceCreated()
}
/**
* Removes CurlMesh from this renderer.
*/
@Synchronized
fun removeCurlMesh(mesh: CurlMesh) {
mCurlMeshes.remove(mesh)
}
/**
* Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
* SHOW_TWO_PAGES.
*/
@Synchronized
fun setViewMode(viewMode: Int) {
if (viewMode == SHOW_ONE_PAGE) {
mViewMode = viewMode
updatePageRect()
} else if (viewMode == SHOW_TWO_PAGES) {
mViewMode = viewMode
updatePageRect()
}
}
/**
* Translates screen coordinates into view coordinates.
*/
fun translate(pt: PointF) {
pt.x = mViewRect.left + mViewRect.width() * pt.x / mViewportWidth
pt.y = mViewRect.top - -mViewRect.height() * pt.y / mViewportHeight
}
/**
* Recalculates page rectangles.
*/
private fun updatePageRect() {
if (mViewRect.width() == 0f || mViewRect.height() == 0f) {
return
} else if (mViewMode == SHOW_ONE_PAGE) {
mPageRectRight.set(mViewRect)
mPageRectRight.left += mViewRect.width() * mMargins.left
mPageRectRight.right -= mViewRect.width() * mMargins.right
mPageRectRight.top += mViewRect.height() * mMargins.top
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom
mPageRectLeft.set(mPageRectRight)
mPageRectLeft.offset(-mPageRectRight.width(), 0f)
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
mObserver.onPageSizeChanged(bitmapW, bitmapH)
} else if (mViewMode == SHOW_TWO_PAGES) {
mPageRectRight.set(mViewRect)
mPageRectRight.left += mViewRect.width() * mMargins.left
mPageRectRight.right -= mViewRect.width() * mMargins.right
mPageRectRight.top += mViewRect.height() * mMargins.top
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom
mPageRectLeft.set(mPageRectRight)
mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2
mPageRectRight.left = mPageRectLeft.right
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
mObserver.onPageSizeChanged(bitmapW, bitmapH)
}
}
/**
* Observer for waiting render engine/state updates.
*/
interface Observer {
/**
* Called from onDrawFrame called before rendering is started. This is
* intended to be used for animation purposes.
*/
fun onDrawFrame()
/**
* Called once page size is changed. Width and height tell the page size
* in pixels making it possible to update textures accordingly.
*/
fun onPageSizeChanged(width: Int, height: Int)
/**
* Called from onSurfaceCreated to enable texture re-initialization etc
* what needs to be done when this happens.
*/
fun onSurfaceCreated()
var canDraw: Boolean
}
companion object {
// Constant for requesting left page rect.
const val PAGE_LEFT = 1
// Constant for requesting right page rect.
const val PAGE_RIGHT = 2
// Constants for changing view mode.
const val SHOW_ONE_PAGE = 1
const val SHOW_TWO_PAGES = 2
}
}

@ -1,769 +0,0 @@
package io.legado.app.ui.book.read.page.curl
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.PointF
import android.opengl.GLSurfaceView
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt
/**
* OpenGL ES View.
*
* @author harism
*/
class CurlView : GLSurfaceView, View.OnTouchListener, CurlRenderer.Observer {
var callBack: CallBack? = null
private var mAllowLastPageCurl = true
private var mAnimate = false
private val mAnimationDurationTime: Long = 300
private val mAnimationSource = PointF()
private var mAnimationStartTime: Long = 0
private val mAnimationTarget = PointF()
private var mAnimationTargetEvent: Int = 0
private val mCurlDir = PointF()
private val mCurlPos = PointF()
private var mCurlState = CURL_NONE
// Current bitmap index. This is always showed as front of right page.
private var mCurrentIndex = 0
// Start position for dragging.
private val mDragStartPos = PointF()
private var mEnableTouchPressure = false
// Bitmap size. These are updated from renderer once it's initialized.
private var mPageBitmapHeight = -1
private var mPageBitmapWidth = -1
// Page meshes. Left and right meshes are 'static' while curl is used to
// show page flipping.
private var mPageCurl: CurlMesh
private var mPageLeft: CurlMesh
private var mPageRight: CurlMesh
private val mPointerPos = PointerPosition()
private var mRenderer: CurlRenderer = CurlRenderer(this)
private var mRenderLeftPage = true
private var mSizeChangedObserver: SizeChangedObserver? = null
// One page is the default.
private var mViewMode = SHOW_ONE_PAGE
var mPageProvider: PageProvider? = null
set(value) {
field = value
mCurrentIndex = 0
updatePages()
requestRender()
}
/**
* Get current page index. Page indices are zero based values presenting
* page being shown on right side of the book.
*/
/**
* Set current page index. Page indices are zero based values presenting
* page being shown on right side of the book. E.g if you set value to 4;
* right side front facing bitmap will be with index 4, back facing 5 and
* for left side page index 3 is front facing, and index 2 back facing (once
* page is on left side it's flipped over).
*
*
* Current index is rounded to closest value divisible with 2.
*/
var currentIndex: Int
get() = mCurrentIndex
set(index) {
mCurrentIndex = if (mPageProvider == null || index < 0) {
0
} else {
if (mAllowLastPageCurl) {
min(index, mPageProvider!!.pageCount)
} else {
min(index, mPageProvider!!.pageCount - 1)
}
}
updatePages()
requestRender()
}
/**
* Default constructor.
*/
constructor(ctx: Context) : super(ctx)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int) : this(ctx, attrs)
/**
* Initialize method.
*/
init {
setEGLConfigChooser(8, 8, 8, 8, 16, 0)
holder.setFormat(PixelFormat.TRANSLUCENT)
setZOrderOnTop(true)
setRenderer(mRenderer)
renderMode = RENDERMODE_WHEN_DIRTY
setOnTouchListener(this)
// Even though left and right pages are static we have to allocate room
// for curl on them too as we are switching meshes. Another way would be
// to swap texture ids only.
mPageLeft = CurlMesh(10)
mPageRight = CurlMesh(10)
mPageCurl = CurlMesh(10)
mPageLeft.setFlipTexture(true)
mPageRight.setFlipTexture(false)
}
override var canDraw: Boolean = false
override fun onDrawFrame() {
// We are not animating.
if (!mAnimate) {
return
}
val currentTime = System.currentTimeMillis()
// If animation is done.
if (currentTime >= mAnimationStartTime + mAnimationDurationTime) {
if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) {
// Switch curled page to right.
val right = mPageCurl
val curl = mPageRight
right.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
right.setFlipTexture(false)
right.reset()
mRenderer.removeCurlMesh(curl)
mPageCurl = curl
mPageRight = right
// If we were curling left page update current index.
if (mCurlState == CURL_LEFT) {
--mCurrentIndex
callBack?.pageChange(-1)
}
canDraw = false
} else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) {
// Switch curled page to left.
val left = mPageCurl
val curl = mPageLeft
left.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
left.setFlipTexture(true)
left.reset()
mRenderer.removeCurlMesh(curl)
if (!mRenderLeftPage) {
mRenderer.removeCurlMesh(left)
}
mPageCurl = curl
mPageLeft = left
// If we were curling right page update current index.
if (mCurlState == CURL_RIGHT) {
++mCurrentIndex
callBack?.pageChange(1)
}
canDraw = false
}
mCurlState = CURL_NONE
mAnimate = false
requestRender()
} else {
mPointerPos.mPos.set(mAnimationSource)
var t = 1f - (currentTime - mAnimationStartTime).toFloat() / mAnimationDurationTime
t = 1f - t * t * t * (3 - 2 * t)
mPointerPos.mPos.x += (mAnimationTarget.x - mAnimationSource.x) * t
mPointerPos.mPos.y += (mAnimationTarget.y - mAnimationSource.y) * t
updateCurlPos(mPointerPos)
}
}
override fun onPageSizeChanged(width: Int, height: Int) {
mPageBitmapWidth = width
mPageBitmapHeight = height
updatePages()
requestRender()
}
public override fun onSizeChanged(w: Int, h: Int, ow: Int, oh: Int) {
super.onSizeChanged(w, h, ow, oh)
requestRender()
if (mSizeChangedObserver != null) {
mSizeChangedObserver!!.onSizeChanged(w, h)
}
}
override fun onSurfaceCreated() {
// In case surface is recreated, let page meshes drop allocated texture
// ids and ask for new ones. There's no need to set textures here as
// onPageSizeChanged should be called later on.
mPageLeft.resetTexture()
mPageRight.resetTexture()
mPageCurl.resetTexture()
}
override fun onTouch(view: View, me: MotionEvent): Boolean {
// No dragging during animation at the moment.
if (mAnimate || mPageProvider == null) {
return false
}
// We need page rects quite extensively so get them for later use.
val rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
val leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)
// Store pointer position.
mPointerPos.mPos.set(me.x, me.y)
mRenderer.translate(mPointerPos.mPos)
if (mEnableTouchPressure) {
mPointerPos.mPressure = me.pressure
} else {
mPointerPos.mPressure = 0.8f
}
when (me.action) {
MotionEvent.ACTION_DOWN -> {
run {
// Once we receive pointer down event its position is mapped to
// right or left edge of page and that'll be the position from where
// user is holding the paper to make curl happen.
mDragStartPos.set(mPointerPos.mPos)
// First we make sure it's not over or below page. Pages are
// supposed to be same height so it really doesn't matter do we use
// left or right one.
if (mDragStartPos.y > rightRect!!.top) {
mDragStartPos.y = rightRect.top
} else if (mDragStartPos.y < rightRect.bottom) {
mDragStartPos.y = rightRect.bottom
}
// Then we have to make decisions for the user whether curl is going
// to happen from left or right, and on which page.
if (mViewMode == SHOW_TWO_PAGES) {
// If we have an open book and pointer is on the left from right
// page we'll mark drag position to left edge of left page.
// Additionally checking mCurrentIndex is higher than zero tells
// us there is a visible page at all.
if (mDragStartPos.x < rightRect.left && mCurrentIndex > 0) {
mDragStartPos.x = leftRect!!.left
startCurl(CURL_LEFT)
} else if (mDragStartPos.x >= rightRect.left && mCurrentIndex < mPageProvider!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 1) {
return false
}
startCurl(CURL_RIGHT)
}// Otherwise check pointer is on right page's side.
} else if (mViewMode == SHOW_ONE_PAGE) {
val halfX = (rightRect.right + rightRect.left) / 2
if (mDragStartPos.x < halfX && mCurrentIndex > 0) {
mDragStartPos.x = rightRect.left
startCurl(CURL_LEFT)
} else if (mDragStartPos.x >= halfX && mCurrentIndex < mPageProvider!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 1) {
return false
}
startCurl(CURL_RIGHT)
}
}
// If we have are in curl state, let this case clause flow through
// to next one. We have pointer position and drag position defined
// and this will create first render request given these points.
if (mCurlState == CURL_NONE) {
return false
}
}
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_MOVE -> {
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (mCurlState == CURL_LEFT || mCurlState == CURL_RIGHT) {
// Animation source is the point from where animation starts.
// Also it's handled in a way we actually simulate touch events
// meaning the output is exactly the same as if user drags the
// page to other side. While not producing the best looking
// result (which is easier done by altering curl position and/or
// direction directly), this is done in a hope it made code a
// bit more readable and easier to maintain.
mAnimationSource.set(mPointerPos.mPos)
mAnimationStartTime = System.currentTimeMillis()
// Given the explanation, here we decide whether to simulate
// drag to left or right end.
if (mViewMode == SHOW_ONE_PAGE && mPointerPos.mPos.x > (rightRect!!.left + rightRect.right) / 2 || mViewMode == SHOW_TWO_PAGES && mPointerPos.mPos.x > rightRect!!.left) {
// On right side target is always right page's right border.
mAnimationTarget.set(mDragStartPos)
mAnimationTarget.x = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT)!!.right
mAnimationTargetEvent = SET_CURL_TO_RIGHT
} else {
// On left side target depends on visible pages.
mAnimationTarget.set(mDragStartPos)
if (mCurlState == CURL_RIGHT || mViewMode == SHOW_TWO_PAGES) {
mAnimationTarget.x = leftRect!!.left
} else {
mAnimationTarget.x = rightRect!!.left
}
mAnimationTargetEvent = SET_CURL_TO_LEFT
}
mAnimate = true
requestRender()
}
}
}
return true
}
/**
* Allow the last page to curl.
*/
fun setAllowLastPageCurl(allowLastPageCurl: Boolean) {
mAllowLastPageCurl = allowLastPageCurl
}
/**
* Sets mPageCurl curl position.
*/
private fun setCurlPos(curlPos: PointF, curlDir: PointF, radius: Double) {
// First reposition curl so that page doesn't 'rip off' from book.
if (mCurlState == CURL_RIGHT || mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE) {
val pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
if (curlPos.x >= pageRect!!.right) {
mPageCurl.reset()
requestRender()
return
}
if (curlPos.x < pageRect.left) {
curlPos.x = pageRect.left
}
if (curlDir.y != 0f) {
val diffX = curlPos.x - pageRect.left
val leftY = curlPos.y + diffX * curlDir.x / curlDir.y
if (curlDir.y < 0 && leftY < pageRect.top) {
curlDir.x = curlPos.y - pageRect.top
curlDir.y = pageRect.left - curlPos.x
} else if (curlDir.y > 0 && leftY > pageRect.bottom) {
curlDir.x = pageRect.bottom - curlPos.y
curlDir.y = curlPos.x - pageRect.left
}
}
} else if (mCurlState == CURL_LEFT) {
val pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)
if (curlPos.x <= pageRect!!.left) {
mPageCurl.reset()
requestRender()
return
}
if (curlPos.x > pageRect.right) {
curlPos.x = pageRect.right
}
if (curlDir.y != 0f) {
val diffX = curlPos.x - pageRect.right
val rightY = curlPos.y + diffX * curlDir.x / curlDir.y
if (curlDir.y < 0 && rightY < pageRect.top) {
curlDir.x = pageRect.top - curlPos.y
curlDir.y = curlPos.x - pageRect.right
} else if (curlDir.y > 0 && rightY > pageRect.bottom) {
curlDir.x = curlPos.y - pageRect.bottom
curlDir.y = pageRect.right - curlPos.x
}
}
}
// Finally normalize direction vector and do rendering.
val dist = sqrt((curlDir.x * curlDir.x + curlDir.y * curlDir.y).toDouble())
if (dist != 0.0) {
curlDir.x /= dist.toFloat()
curlDir.y /= dist.toFloat()
mPageCurl.curl(curlPos, curlDir, radius)
} else {
mPageCurl.reset()
}
requestRender()
}
/**
* If set to true, touch event pressure information is used to adjust curl
* radius. The more you press, the flatter the curl becomes. This is
* somewhat experimental and results may vary significantly between devices.
* On emulator pressure information seems to be flat 1.0f which is maximum
* value and therefore not very much of use.
*/
fun setEnableTouchPressure(enableTouchPressure: Boolean) {
mEnableTouchPressure = enableTouchPressure
}
/**
* Setter for whether left side page is rendered. This is useful mostly for
* situations where right (main) page is aligned to left side of screen and
* left page is not visible anyway.
*/
fun setRenderLeftPage(renderLeftPage: Boolean) {
mRenderLeftPage = renderLeftPage
}
/**
* Sets SizeChangedObserver for this View. Call back method is called from
* this View's onSizeChanged method.
*/
fun setSizeChangedObserver(observer: SizeChangedObserver) {
mSizeChangedObserver = observer
}
/**
* Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In
* former case right page is made size of display, and in latter case two
* pages are laid on visible area.
*/
fun setViewMode(viewMode: Int) {
when (viewMode) {
SHOW_ONE_PAGE -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(true)
mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE)
}
SHOW_TWO_PAGES -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(false)
mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES)
}
}
}
/**
* Switches meshes and loads new bitmaps if available. Updated to support 2
* pages in landscape
*/
private fun startCurl(page: Int) {
when (page) {
// Once right side page is curled, first right page is assigned into
// curled page. And if there are more bitmaps available new bitmap is
// loaded into right side mesh.
CURL_RIGHT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling right page.
val curl = mPageRight
mPageRight = mPageCurl
mPageCurl = curl
if (mCurrentIndex > 0) {
mPageLeft.setFlipTexture(true)
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
if (mCurrentIndex < mPageProvider!!.pageCount - 1) {
updatePage(mPageRight.texturePage, mCurrentIndex + 1)
mPageRight.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageRight.setFlipTexture(false)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
// Add curled page to renderer.
mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
mPageCurl.setFlipTexture(false)
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
mCurlState = CURL_RIGHT
}
// On left side curl, left page is assigned to curled page. And if
// there are more bitmaps available before currentIndex, new bitmap
// is loaded into left page.
CURL_LEFT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling left page.
val curl = mPageLeft
mPageLeft = mPageCurl
mPageCurl = curl
if (mCurrentIndex > 1) {
updatePage(mPageLeft.texturePage, mCurrentIndex - 2)
mPageLeft.setFlipTexture(true)
mPageLeft
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
// If there is something to show on right page add it to renderer.
if (mCurrentIndex < mPageProvider!!.pageCount) {
mPageRight.setFlipTexture(false)
mPageRight.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
// How dragging previous page happens depends on view mode.
if (mViewMode == SHOW_ONE_PAGE || mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES) {
mPageCurl.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
mPageCurl.setFlipTexture(false)
} else {
mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageCurl.setFlipTexture(true)
}
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
mCurlState = CURL_LEFT
}
}
}
/**
* Updates curl position.
*/
private fun updateCurlPos(pointerPos: PointerPosition) {
// Default curl radius.
var radius = (mRenderer.getPageRect(CURL_RIGHT)!!.width() / 3).toDouble()
// TODO: This is not an optimal solution. Based on feedback received so
// far; pressure is not very accurate, it may be better not to map
// coefficient to range [0f, 1f] but something like [.2f, 1f] instead.
// Leaving it as is until get my hands on a real device. On emulator
// this doesn't work anyway.
radius *= max(1f - pointerPos.mPressure, 0f).toDouble()
// NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing
// later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's
// actually pointerPos we are doing calculations against. Why? Simply to
// optimize code a bit with the cost of making it unreadable. Otherwise
// we had to this in both of the next if-else branches.
mCurlPos.set(pointerPos.mPos)
// If curl happens on right page, or on left page on two page mode,
// we'll calculate curl position from pointerPos.
if (mCurlState == CURL_RIGHT || mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES) {
mCurlDir.x = mCurlPos.x - mDragStartPos.x
mCurlDir.y = mCurlPos.y - mDragStartPos.y
val dist =
sqrt((mCurlDir.x * mCurlDir.x + mCurlDir.y * mCurlDir.y).toDouble()).toFloat()
// Adjust curl radius so that if page is dragged far enough on
// opposite side, radius gets closer to zero.
val pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
.width()
var curlLen = radius * Math.PI
if (dist > pageWidth * 2 - curlLen) {
curlLen = max(pageWidth * 2 - dist, 0f).toDouble()
radius = curlLen / Math.PI
}
// Actual curl position calculation.
if (dist >= curlLen) {
val translate = (dist - curlLen) / 2
if (mViewMode == SHOW_TWO_PAGES) {
mCurlPos.x -= (mCurlDir.x * translate / dist).toFloat()
} else {
val pageLeftX = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(
min((mCurlPos.x - pageLeftX).toDouble(), radius),
0.0
)
}
mCurlPos.y -= (mCurlDir.y * translate / dist).toFloat()
} else {
val angle = Math.PI * sqrt(dist / curlLen)
val translate = radius * sin(angle)
mCurlPos.x += (mCurlDir.x * translate / dist).toFloat()
mCurlPos.y += (mCurlDir.y * translate / dist).toFloat()
}
} else if (mCurlState == CURL_LEFT) {
// Adjust radius regarding how close to page edge we are.
val pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(min((mCurlPos.x - pageLeftX).toDouble(), radius), 0.0)
val pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.right
mCurlPos.x -= min((pageRightX - mCurlPos.x).toDouble(), radius).toFloat()
mCurlDir.x = mCurlPos.x + mDragStartPos.x
mCurlDir.y = mCurlPos.y - mDragStartPos.y
}// Otherwise we'll let curl follow pointer position.
setCurlPos(mCurlPos, mCurlDir, radius)
}
/**
* Updates given CurlPage via PageProvider for page located at index.
*/
private fun updatePage(page: CurlPage, index: Int) {
// First reset page to initial state.
page.reset()
// Ask page provider to fill it up with bitmaps and colors.
mPageProvider!!.updatePage(
page, mPageBitmapWidth, mPageBitmapHeight,
index
)
}
/**
* Updates bitmaps for page meshes.
*/
fun updatePages() {
if (mPageProvider == null || mPageBitmapWidth <= 0
|| mPageBitmapHeight <= 0
) {
return
}
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
var leftIdx = mCurrentIndex - 1
var rightIdx = mCurrentIndex
var curlIdx = -1
if (mCurlState == CURL_LEFT) {
curlIdx = leftIdx
--leftIdx
} else if (mCurlState == CURL_RIGHT) {
curlIdx = rightIdx
++rightIdx
}
if (rightIdx >= 0 && rightIdx < mPageProvider!!.pageCount) {
updatePage(mPageRight.texturePage, rightIdx)
mPageRight.setFlipTexture(false)
mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
if (leftIdx >= 0 && leftIdx < mPageProvider!!.pageCount) {
updatePage(mPageLeft.texturePage, leftIdx)
mPageLeft.setFlipTexture(true)
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
if (curlIdx >= 0 && curlIdx < mPageProvider!!.pageCount) {
updatePage(mPageCurl.texturePage, curlIdx)
if (mCurlState == CURL_RIGHT) {
mPageCurl.setFlipTexture(true)
mPageCurl.setRect(
mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
)
} else {
mPageCurl.setFlipTexture(false)
mPageCurl
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
}
mPageCurl.reset()
mRenderer.addCurlMesh(mPageCurl)
}
}
/**
* Provider for feeding 'book' with bitmaps which are used for rendering
* pages.
*/
interface PageProvider {
/**
* Return number of pages available.
*/
val pageCount: Int
/**
* Called once new bitmaps/textures are needed. Width and height are in
* pixels telling the size it will be drawn on screen and following them
* ensures that aspect ratio remains. But it's possible to return bitmap
* of any size though. You should use provided CurlPage for storing page
* information for requested page number.<br></br>
* <br></br>
* Index is a number between 0 and getBitmapCount() - 1.
*/
fun updatePage(page: CurlPage, width: Int, height: Int, index: Int)
}
/**
* Simple holder for pointer position.
*/
private inner class PointerPosition {
internal var mPos = PointF()
internal var mPressure: Float = 0.toFloat()
}
/**
* Observer interface for handling CurlView size changes.
*/
interface SizeChangedObserver {
/**
* Called once CurlView size changes.
*/
fun onSizeChanged(width: Int, height: Int)
}
interface CallBack {
fun pageChange(change: Int)
}
companion object {
// Curl state. We are flipping none, left or right page.
private const val CURL_LEFT = 1
private const val CURL_NONE = 0
private const val CURL_RIGHT = 2
// Constants for mAnimationTargetEvent.
private const val SET_CURL_TO_LEFT = 1
private const val SET_CURL_TO_RIGHT = 2
// Shows one page at the center of view.
const val SHOW_ONE_PAGE = 1
// Shows two pages side by side.
const val SHOW_TWO_PAGES = 2
}
}

@ -39,11 +39,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
} }
} }
if (isMoved) { if (isMoved) {
isCancel = if (pageView.isScrollDelegate) { isCancel = if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0
} else {
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0
}
isRunning = true isRunning = true
//设置触摸点 //设置触摸点
setTouchPoint(e2.x, e2.y) setTouchPoint(e2.x, e2.y)

@ -69,7 +69,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
var isRunning = false var isRunning = false
var isStarted = false var isStarted = false
protected fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x startX = x
startY = y startY = y
@ -78,7 +78,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
} }
} }
protected fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
touchX = x touchX = x
touchY = y touchY = y

@ -109,11 +109,7 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
curPage?.dispatchTouchEvent(e2) curPage?.dispatchTouchEvent(e2)
} }
if (isMoved) { if (isMoved) {
isCancel = if (pageView.isScrollDelegate) { isCancel = if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0
} else {
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0
}
isRunning = true isRunning = true
//设置触摸点 //设置触摸点
setTouchPoint(e2.x, e2.y) setTouchPoint(e2.x, e2.y)

@ -1,126 +1,526 @@
package io.legado.app.ui.book.read.page.delegate package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas import android.graphics.*
import android.view.MotionEvent import android.graphics.drawable.GradientDrawable
import android.os.Build
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.curl.CurlPage import kotlin.math.*
import io.legado.app.ui.book.read.page.curl.CurlView
import io.legado.app.utils.screenshot
import kotlin.math.abs
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView), class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
CurlView.CallBack {
var curlView: CurlView? = null private var mCornerX = 1 // 拖拽点对应的页脚
private var mCornerY = 1
private val mPath0: Path = Path()
private val mPath1: Path = Path()
// 贝塞尔曲线起始点
private val mBezierStart1 = PointF()
// 贝塞尔曲线控制点
private val mBezierControl1 = PointF()
// 贝塞尔曲线顶点
private val mBezierVertex1 = PointF()
// 贝塞尔曲线结束点
private var mBezierEnd1 = PointF()
// 另一条贝塞尔曲线
private val mBezierStart2 = PointF()
private val mBezierControl2 = PointF()
private val mBezierVertex2 = PointF()
private var mBezierEnd2 = PointF()
private var mMiddleX = 0f
private var mMiddleY = 0f
private var mDegrees = 0f
private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter? = null
private val mMatrix: Matrix = Matrix()
private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f)
// 是否属于右上左下
private var mIsRT_LB = false
private var mMaxLength = 0f
// 背面颜色组
private var mBackShadowColors: IntArray? = null
// 前面颜色组
private var mFrontShadowColors: IntArray? = null
// 有阴影的GradientDrawable
private var mBackShadowDrawableLR: GradientDrawable? = null
private var mBackShadowDrawableRL: GradientDrawable? = null
private var mFolderShadowDrawableLR: GradientDrawable? = null
private var mFolderShadowDrawableRL: GradientDrawable? = null
private var mFrontShadowDrawableHBT: GradientDrawable? = null
private var mFrontShadowDrawableHTB: GradientDrawable? = null
private var mFrontShadowDrawableVLR: GradientDrawable? = null
private var mFrontShadowDrawableVRL: GradientDrawable? = null
private val mPaint: Paint = Paint()
init { init {
pageView.curlView ?: let { mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat()
curlView = CurlView(pageView.context) mPaint.style = Paint.Style.FILL
pageView.curlView = curlView //设置颜色数组
pageView.addView(curlView) createDrawable()
curlView?.mPageProvider = PageProvider() val cm = ColorMatrix()
curlView?.setSizeChangedObserver(SizeChangedObserver()) val array = floatArrayOf(
curlView?.callBack = this 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f,
0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f
)
cm.set(array)
mColorMatrixFilter = ColorMatrixColorFilter(cm)
} }
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
super.setStartPoint(x, y, invalidate)
calcCornerXY(x, y)
} }
override fun onTouch(event: MotionEvent): Boolean { override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
when (event.action) { super.setTouchPoint(x, y, invalidate)
MotionEvent.ACTION_DOWN -> { //触摸y中间位置吧y变成屏幕高度
curlView?.currentIndex = 1 //触摸y中间位置吧y变成屏幕高度
if (startY > pageView.height / 3.0
&& startY < pageView.height * 2 / 3.0
|| direction == Direction.PREV
) {
touchY = pageView.height.toFloat()
} }
if (startY > pageView.height / 3.0
&& startY < pageView.height / 2.0
&& direction == Direction.NEXT
) {
touchY = 1f
} }
curlView?.dispatchTouchEvent(event)
return super.onTouch(event)
} }
override fun onScrollStart() { override fun onScrollStart() {
val distanceX: Float
when (direction) {
Direction.NEXT -> distanceX =
if (isCancel) {
var dis = viewWidth - startX + touchX
if (dis > viewWidth) {
dis = viewWidth.toFloat()
}
viewWidth - dis
} else {
-(touchX + (viewWidth - startX))
}
else -> distanceX =
if (isCancel) {
-(touchX - startX)
} else {
viewWidth - (touchX - startX)
}
} }
override fun onDraw(canvas: Canvas) { startScroll(touchX.toInt(), 0, distanceX.toInt(), 0)
} }
override fun onScrollStop() { override fun onScrollStop() {
curPage?.x = 0.toFloat()
if (!isCancel) {
pageView.fillPage(direction)
} }
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isMoved) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage?.dispatchTouchEvent(event)
event.recycle()
if (abs(distanceX) > abs(distanceY)) {
if (distanceX < 0) {
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
} }
//上一页截图
bitmap = prevPage?.screenshot() override fun onDraw(canvas: Canvas) {
bitmap?.let {
if (direction === Direction.NEXT) {
calcPoints()
drawCurrentPageArea(canvas, it, mPath0) //绘制翻页时的正面页
drawNextPageAreaAndShadow(canvas, it)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
} else { } else {
//如果不存在表示没有下一页了 calcPoints()
if (!hasNext()) { drawCurrentPageArea(canvas, it, mPath0)
noNext = true drawNextPageAreaAndShadow(canvas, it)
return true drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
} }
//下一页截图
bitmap = nextPage?.screenshot()
} }
isMoved = true
} }
/**
* 创建阴影的GradientDrawable
*/
private fun createDrawable() {
val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFolderShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111)
mBackShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111)
mFrontShadowDrawableVLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableVRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHTB = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHBT = GradientDrawable(
GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
} }
if (isMoved) {
curlView?.canDraw = true /**
isCancel = if (pageView.isScrollDelegate) { * 绘制翻起页背面
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 */
private fun drawCurrentBackArea(
canvas: Canvas,
bitmap: Bitmap
) {
val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2
val f1 = abs(i - mBezierControl1.x)
val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2
val f2 = abs(i1 - mBezierControl2.y)
val f3 = min(f1, f2)
mPath1.reset()
mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath1.close()
val mFolderShadowDrawable: GradientDrawable
val left: Int
val right: Int
if (mIsRT_LB) {
left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR!!
} else { } else {
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 left = (mBezierStart1.x - f3 - 1).toInt()
right = (mBezierStart1.x + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableRL!!
} }
isRunning = true canvas.save()
//设置触摸点 try {
setTouchPoint(e2.x, e2.y) canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
canvas.clipPath(mPath1, Region.Op.INTERSECT)
} }
return isMoved } catch (ignored: Exception) {
} }
mPaint.colorFilter = mColorMatrixFilter
override fun onPageUp() { val dis = hypot(
curlView?.updatePages() mCornerX - mBezierControl1.x.toDouble(),
curlView?.requestRender() mBezierControl2.y - mCornerY.toDouble()
).toFloat()
val f8 = (mCornerX - mBezierControl1.x) / dis
val f9 = (mBezierControl2.y - mCornerY) / dis
mMatrixArray[0] = 1 - 2 * f9 * f9
mMatrixArray[1] = 2 * f8 * f9
mMatrixArray[3] = mMatrixArray[1]
mMatrixArray[4] = 1 - 2 * f8 * f8
mMatrix.reset()
mMatrix.setValues(mMatrixArray)
mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y)
mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y)
canvas.drawBitmap(bitmap, mMatrix, mPaint)
mPaint.colorFilter = null
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mFolderShadowDrawable.setBounds(
left, mBezierStart1.y.toInt(), right,
(mBezierStart1.y + mMaxLength).toInt()
)
mFolderShadowDrawable.draw(canvas)
canvas.restore()
} }
override fun pageChange(change: Int) { /**
pageView.post { * 绘制翻起页的阴影
if (change > 0) { */
pageView.moveToNextPage() private fun drawCurrentPageShadow(canvas: Canvas) {
val degree: Double = if (mIsRT_LB) {
(Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x))
} else {
(Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x))
}
// 翻起页阴影顶点与touch点的距离
val d1 = 25.toFloat() * 1.414 * cos(degree)
val d2 = 25.toFloat() * 1.414 * sin(degree)
val x = (touchX + d1).toFloat()
val y: Float
y = if (mIsRT_LB) {
(touchY + d2).toFloat()
} else {
(touchY - d2).toFloat()
}
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl1.x, mBezierControl1.y)
mPath1.lineTo(mBezierStart1.x, mBezierStart1.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else { } else {
pageView.moveToPrevPage() canvas.clipPath(mPath0, Region.Op.XOR)
} }
canvas.clipPath(mPath1, Region.Op.INTERSECT)
} catch (ignored: java.lang.Exception) {
} }
var leftX: Int
var rightX: Int
var mCurrentPageShadow: GradientDrawable
if (mIsRT_LB) {
leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25
mCurrentPageShadow = mFrontShadowDrawableVLR!!
} else {
leftX = (mBezierControl1.x - 25).toInt()
rightX = mBezierControl1.x.toInt() + 1
mCurrentPageShadow = mFrontShadowDrawableVRL!!
}
var rotateDegrees: Float =
Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble())
.toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
mCurrentPageShadow.setBounds(
leftX,
(mBezierControl1.y - mMaxLength).toInt(), rightX,
mBezierControl1.y.toInt()
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl2.x, mBezierControl2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.clipPath(mPath1)
} catch (ignored: java.lang.Exception) {
}
if (mIsRT_LB) {
leftX = mBezierControl2.y.toInt()
rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB!!
} else {
leftX = (mBezierControl2.y - 25).toInt()
rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT!!
}
rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float =
if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt()
if (hmg > mMaxLength) mCurrentPageShadow
.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg,
rightX
) else mCurrentPageShadow.setBounds(
(mBezierControl2.x - mMaxLength).toInt(), leftX,
mBezierControl2.x.toInt(), rightX
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
} }
private inner class PageProvider : CurlView.PageProvider { private fun drawNextPageAreaAndShadow(
canvas: Canvas,
bitmap: Bitmap
) {
mPath1.reset()
mPath1.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath1.close()
mDegrees = Math.toDegrees(
atan2(
(mBezierControl1.x - mCornerX).toDouble(),
mBezierControl2.y - mCornerY.toDouble()
)
).toFloat()
val leftX: Int
val rightY: Int
val mBackShadowDrawable: GradientDrawable
if (mIsRT_LB) { //左下及右上
leftX = mBezierStart1.x.toInt()
rightY = (mBezierStart1.x + mTouchToCornerDis / 4).toInt()
mBackShadowDrawable = mBackShadowDrawableLR!!
} else {
leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt()
rightY = mBezierStart1.x.toInt()
mBackShadowDrawable = mBackShadowDrawableRL!!
}
canvas.save()
try {
canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
canvas.clipPath(mPath1, Region.Op.INTERSECT)
}
//canvas.clipPath(mPath1, Region.Op.INTERSECT);
} catch (ignored: java.lang.Exception) {
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mBackShadowDrawable.setBounds(
leftX, mBezierStart1.y.toInt(), rightY,
(mMaxLength + mBezierStart1.y).toInt()
) //左上及右下角的xy坐标值,构成一个矩形
mBackShadowDrawable.draw(canvas)
canvas.restore()
}
override val pageCount: Int private fun drawCurrentPageArea(
get() = 3 canvas: Canvas,
bitmap: Bitmap,
path: Path
) {
mPath0.reset()
mPath0.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath0.quadTo(
mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x,
mBezierEnd1.y
)
mPath0.lineTo(touchX, touchY)
mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath0.quadTo(
mBezierControl2.x, mBezierControl2.y, mBezierStart2.x,
mBezierStart2.y
)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close()
canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path)
} else {
canvas.clipPath(path, Region.Op.XOR)
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
try {
canvas.restore()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
override fun updatePage(page: CurlPage, width: Int, height: Int, index: Int) { /**
when (index) { * 计算拖拽点对应的拖拽脚
0 -> page.setTexture(prevPage?.screenshot(), CurlPage.SIDE_BOTH) */
1 -> page.setTexture(curPage?.screenshot(), CurlPage.SIDE_BOTH) private fun calcCornerXY(x: Float, y: Float) {
2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH) mCornerX = if (x <= pageView.width / 2.0) {
0
} else {
pageView.width
} }
mCornerY = if (y <= pageView.height / 2.0) {
0
} else {
pageView.height
} }
mIsRT_LB = (mCornerX == 0 && mCornerY == pageView.height
|| mCornerX == pageView.width && mCornerY == 0)
} }
// 定义书籍尺寸的变化监听器 private fun calcPoints() {
private inner class SizeChangedObserver : CurlView.SizeChangedObserver { mMiddleX = (touchX + mCornerX) / 2
override fun onSizeChanged(width: Int, height: Int) { mMiddleY = (touchY + mCornerY) / 2
curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f4 = mCornerY - mMiddleY
if (f4 == 0f) {
mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
} }
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
mBezierStart1.y = mCornerY.toFloat()
// 当mBezierStart1.x < 0或者mBezierStart1.x > 480时
// 如果继续翻页,会出现BUG故在此限制
if (touchX > 0 && touchX < pageView.width) {
if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) {
if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x
val f1: Float = abs(mCornerX - touchX)
val f2: Float = pageView.width * f1 / mBezierStart1.x
touchX = abs(mCornerX - f2)
val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1
touchX = abs(mCornerY - f3)
mMiddleX = (touchX + mCornerX) / 2
mMiddleY = (touchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f5 = mCornerY - mMiddleY
if (f5 == 0f) {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = (mBezierControl1.x
- (mCornerX - mBezierControl1.x) / 2)
}
}
mBezierStart2.x = mCornerX.toFloat()
mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2
mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY)
mBezierEnd1 = getCross(
PointF(touchX, touchY), mBezierControl1, mBezierStart1,
mBezierStart2
)
mBezierEnd2 = getCross(
PointF(touchX, touchY), mBezierControl2, mBezierStart1,
mBezierStart2
)
mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4
mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4
mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4
mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4
}
/**
* 求解直线P1P2和直线P3P4的交点坐标
*/
private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF {
val crossP = PointF()
// 二元函数通式: y=ax+b
val a1 = (P2.y - P1.y) / (P2.x - P1.x)
val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x)
val a2 = (P4.y - P3.y) / (P4.x - P3.x)
val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x)
crossP.x = (b2 - b1) / (a1 - a2)
crossP.y = a1 * crossP.x + b1
return crossP
} }
} }

@ -91,7 +91,7 @@ class BookSourceEditActivity :
GSON.toJson(getSource())?.let { sourceStr -> GSON.toJson(getSource())?.let { sourceStr ->
try { try {
val textIntent = Intent(Intent.ACTION_SEND) val textIntent = Intent(Intent.ACTION_SEND)
textIntent.setType("text/plain") textIntent.type = "text/plain"
textIntent.putExtra(Intent.EXTRA_TEXT, sourceStr) textIntent.putExtra(Intent.EXTRA_TEXT, sourceStr)
startActivity(Intent.createChooser(textIntent, "Source Share")) startActivity(Intent.createChooser(textIntent, "Source Share"))
} catch (e: Exception) { } catch (e: Exception) {

@ -149,9 +149,11 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
importSource(file.readText(), finally) importSource(file.readText(), finally)
} else { } else {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
finally("文件无法打开") finally("打开文件出错")
} }
} }
}.onError {
finally(it.localizedMessage ?: "打开文件出错")
} }
} }
@ -194,7 +196,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
else -> "格式不对" else -> "格式不对"
} }
}.onError { }.onError {
finally(it.localizedMessage) finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it ?: "导入完成") finally(it ?: "导入完成")
} }

@ -13,6 +13,7 @@ import io.legado.app.R
import io.legado.app.base.VMBaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.getViewModelOfActivity
import kotlinx.android.synthetic.main.fragment_chapter_list.* import kotlinx.android.synthetic.main.fragment_chapter_list.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
@ -58,6 +59,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} }
private fun initView() { private fun initView() {
ll_chapter_base_info.setBackgroundColor(backgroundColor)
iv_chapter_top.onClick { recycler_view.scrollToPosition(0) } iv_chapter_top.onClick { recycler_view.scrollToPosition(0) }
iv_chapter_bottom.onClick { iv_chapter_bottom.onClick {
if (adapter.itemCount > 0) { if (adapter.itemCount > 0) {

@ -1,9 +1,13 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -20,13 +24,26 @@ import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { class WebDavConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener,
CoroutineScope {
lateinit var job: Job
private val oldDataRequestCode = 23156
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
job = Job()
fun bindPreferenceSummaryToValue(preference: Preference?) { fun bindPreferenceSummaryToValue(preference: Preference?) {
preference?.apply { preference?.apply {
onPreferenceChangeListener = this@WebDavConfigFragment onPreferenceChangeListener = this@WebDavConfigFragment
@ -64,6 +81,11 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference
ATH.applyEdgeEffectColor(listView) ATH.applyEdgeEffectColor(listView)
} }
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
when { when {
preference?.key == "web_dav_password" -> if (newValue == null) { preference?.key == "web_dav_password" -> if (newValue == null) {
@ -105,7 +127,18 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference
WebDavHelp.showRestoreDialog(requireContext()) WebDavHelp.showRestoreDialog(requireContext())
} }
.request() .request()
"import_old" -> needInstallApps { "import_old" -> importOldData()
}
return super.onPreferenceTreeClick(preference)
}
private fun importOldData() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, oldDataRequestCode)
} catch (e: Exception) {
needInstallApps {
alert(title = "导入") { alert(title = "导入") {
message = "是否导入旧版本数据" message = "是否导入旧版本数据"
yesButton { yesButton {
@ -122,7 +155,54 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference
}.show().applyTint() }.show().applyTint()
} }
} }
return super.onPreferenceTreeClick(preference) }
private fun importOldData(uri: Uri) {
launch(IO) {
DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach {
when (it.name) {
"myBookShelf.json" ->
try {
DocumentUtils.readText(requireContext(), it.uri)?.let { json ->
val importCount = Restore.importOldBookshelf(json)
withContext(Dispatchers.Main) {
requireContext().toast("成功导入书籍${importCount}")
}
}
} catch (e: java.lang.Exception) {
withContext(Dispatchers.Main) {
requireContext().toast("导入书籍失败\n${e.localizedMessage}")
}
}
"myBookSource.json" ->
try {
DocumentUtils.readText(requireContext(), it.uri)?.let { json ->
val importCount = Restore.importOldSource(json)
withContext(Dispatchers.Main) {
requireContext().toast("成功导入书源${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
requireContext().toast("导入源失败\n${e.localizedMessage}")
}
}
"myBookReplaceRule.json" ->
try {
DocumentUtils.readText(requireContext(), it.uri)?.let { json ->
val importCount = Restore.importOldReplaceRule(json)
withContext(Dispatchers.Main) {
requireContext().toast("成功导入替换规则${importCount}")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
requireContext().toast("导入替换规则失败\n${e.localizedMessage}")
}
}
}
}
}
} }
private fun needInstallApps(callback: () -> Unit) { private fun needInstallApps(callback: () -> Unit) {
@ -147,4 +227,15 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference
callback() callback()
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
oldDataRequestCode ->
if (resultCode == RESULT_OK) data?.data?.let { uri ->
importOldData(uri)
}
}
}
} }

@ -1,25 +1,57 @@
package io.legado.app.ui.download package io.legado.app.ui.download
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.constant.Bus
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.service.help.Download
import io.legado.app.utils.applyTint
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.activity_download.* import kotlinx.android.synthetic.main.activity_download.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
class DownloadActivity : BaseActivity(R.layout.activity_download) { class DownloadActivity : BaseActivity(R.layout.activity_download) {
lateinit var adapter: DownloadAdapter lateinit var adapter: DownloadAdapter
private var bookshelfLiveData: LiveData<List<Book>>? = null private var bookshelfLiveData: LiveData<List<Book>>? = null
private var menu: Menu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView() initRecyclerView()
initLiveData() initLiveData()
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.download, menu)
this.menu = menu
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_download -> launch(IO) {
App.db.bookDao().webBooks.forEach { book ->
Download.start(
this@DownloadActivity,
book.bookUrl,
book.durChapterIndex,
book.totalChapterNum
)
}
}
}
return super.onCompatOptionsItemSelected(item)
}
private fun initRecyclerView() { private fun initRecyclerView() {
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
adapter = DownloadAdapter(this) adapter = DownloadAdapter(this)
@ -28,8 +60,22 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) {
private fun initLiveData() { private fun initLiveData() {
bookshelfLiveData?.removeObservers(this) bookshelfLiveData?.removeObservers(this)
bookshelfLiveData = App.db.bookDao().observeDownload()
bookshelfLiveData?.observe(this, Observer { bookshelfLiveData?.observe(this, Observer {
adapter.setItems(it) adapter.setItems(it)
}) })
} }
override fun observeLiveBus() {
observeEvent<Boolean>(Bus.UP_DOWNLOAD) {
if (it) {
menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp)
menu?.applyTint(this)
adapter.notifyItemRangeChanged(0, adapter.itemCount, true)
} else {
menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_play_24dp)
menu?.applyTint(this)
}
}
}
} }

@ -1,18 +1,36 @@
package io.legado.app.ui.download package io.legado.app.ui.download
import android.content.Context import android.content.Context
import android.view.View
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp
import kotlinx.android.synthetic.main.item_download.view.*
class DownloadAdapter(context: Context) : class DownloadAdapter(context: Context) :
SimpleRecyclerAdapter<Book>(context, R.layout.item_download) { SimpleRecyclerAdapter<Book>(context, R.layout.item_download) {
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) {
with(holder.itemView) {
if (payloads.isEmpty()) {
tv_name.text = item.name
tv_author.text = item.author
upDownloadCount(this, item)
} else {
upDownloadCount(this, item)
}
}
}
private fun upDownloadCount(view: View, book: Book) {
view.tv_download.text = context.getString(
R.string.download_count,
BookHelp.getChapterCount(book),
book.totalChapterNum
)
} }
} }

@ -1,8 +1,12 @@
package io.legado.app.ui.main package io.legado.app.ui.main
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
@ -13,7 +17,12 @@ import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.PreferKey
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore
import io.legado.app.help.storage.WebDavHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
@ -28,7 +37,8 @@ import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main), class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
BottomNavigationView.OnNavigationItemSelectedListener, BottomNavigationView.OnNavigationItemSelectedListener,
ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() { ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() {
private val backupSelectRequestCode = 4567489
private val restoreSelectRequestCode = 654872
override val viewModel: MainViewModel override val viewModel: MainViewModel
get() = getViewModel(MainViewModel::class.java) get() = getViewModel(MainViewModel::class.java)
@ -137,6 +147,86 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
} }
fun backup() {
val backupPath = getPrefString(PreferKey.backupPath)
if (backupPath?.isEmpty() == true) {
selectBackupFolder()
} else {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(this, uri)
if (doc?.canWrite() == true) {
Backup.backup(this, uri)
} else {
selectBackupFolder()
}
}
}
fun restore() {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted { WebDavHelp.showRestoreDialog(this) }
.request()
}
fun restore(uri: Uri) {
}
private fun selectBackupFolder() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, backupSelectRequestCode)
} catch (e: java.lang.Exception) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted { Backup.backup() }
.request()
}
}
private fun selectRestoreFolder() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, restoreSelectRequestCode)
} catch (e: java.lang.Exception) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted { Restore.restore() }
.request()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
backupSelectRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
putPrefString(PreferKey.backupPath, uri.toString())
Backup.backup(this, uri)
}
}
restoreSelectRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
putPrefString(PreferKey.backupPath, uri.toString())
}
}
}
}
private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
@ -154,4 +244,3 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
} }

@ -4,7 +4,14 @@ import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.data.api.IHttpGetApi
import io.legado.app.data.entities.RssSource
import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Restore
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.GSON
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -48,4 +55,24 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun initRss() {
execute {
val url = "https://gitee.com/alanskycn/yuedu/raw/master/JS/RSS/rssSource"
NetworkUtils.getBaseUrl(url)?.let {
val response =
HttpHelper.getApiService<IHttpGetApi>(it).getAsync(url, mapOf()).await()
response.body()?.let { body ->
val sources = mutableListOf<RssSource>()
val items: List<Map<String, Any>> = Restore.jsonPath.parse(body).read("$")
for (item in items) {
val jsonItem = Restore.jsonPath.parse(item)
GSON.fromJsonObject<RssSource>(jsonItem.jsonString())?.let { source ->
sources.add(source)
}
}
App.db.rssSourceDao().insert(*sources.toTypedArray())
}
}
}
}
} }

@ -19,6 +19,7 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.download.DownloadActivity
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.putPrefInt import io.legado.app.utils.putPrefInt
@ -62,7 +63,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
R.id.menu_arrange_bookshelf -> { R.id.menu_arrange_bookshelf -> {
} }
R.id.menu_download_all -> viewModel.downloadAll() R.id.menu_download -> startActivity<DownloadActivity>()
} }
} }

@ -4,7 +4,6 @@ import android.app.Application
import io.legado.app.App import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.service.help.Download
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
@ -33,11 +32,4 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun downloadAll() {
execute {
App.db.bookDao().webBooks.forEach { book ->
Download.start(context, book.bookUrl, book.durChapterIndex, book.totalChapterNum)
}
}
}
} }

@ -9,7 +9,6 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import io.legado.app.utils.gone import io.legado.app.utils.gone
@ -29,7 +28,6 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
ATH.applyBackgroundTint(ll_title)
tv_name.text = item.bookSourceName tv_name.text = item.bookSourceName
ll_title.onClick { ll_title.onClick {
val oldEx = exIndex val oldEx = exIndex

@ -7,14 +7,12 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
import io.legado.app.constant.Bus
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.WebDavHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.service.WebService import io.legado.app.service.WebService
import io.legado.app.ui.about.AboutActivity import io.legado.app.ui.about.AboutActivity
@ -22,11 +20,9 @@ import io.legado.app.ui.about.DonateActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.config.ConfigActivity import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.main.MainActivity
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
import io.legado.app.utils.LogUtils import io.legado.app.utils.*
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.startActivity
import io.legado.app.utils.toast
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
@ -47,16 +43,14 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
override fun onCompatOptionsItemSelected(item: MenuItem) { override fun onCompatOptionsItemSelected(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.menu_help -> startActivity<AboutActivity>() R.id.menu_help -> startActivity<AboutActivity>()
R.id.menu_backup -> PermissionsCompat.Builder(this) R.id.menu_backup -> {
.addPermissions(*Permissions.Group.STORAGE) val activity = activity as? MainActivity
.rationale(R.string.tip_perm_request_storage) activity?.backup()
.onGranted { Backup.backup() } }
.request() R.id.menu_restore -> {
R.id.menu_restore -> PermissionsCompat.Builder(this) val activity = activity as? MainActivity
.addPermissions(*Permissions.Group.STORAGE) activity?.restore()
.rationale(R.string.tip_perm_request_storage) }
.onGranted { WebDavHelp.showRestoreDialog(requireContext()) }
.request()
} }
} }
@ -64,7 +58,17 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
if (WebService.isRun) {
putPrefBoolean("webService", true)
} else {
putPrefBoolean("webService", false)
}
addPreferencesFromResource(R.xml.pref_main) addPreferencesFromResource(R.xml.pref_main)
observeEvent<Boolean>(Bus.WEB_SERVICE_STOP) {
findPreference<SwitchPreference>("webService")?.let {
it.isChecked = false
}
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

@ -11,8 +11,10 @@ import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.rss.article.RssArticlesActivity import io.legado.app.ui.rss.article.RssArticlesActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import kotlinx.android.synthetic.main.fragment_rss.* import kotlinx.android.synthetic.main.fragment_rss.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
@ -50,6 +52,9 @@ class RssFragment : BaseFragment(R.layout.fragment_rss),
private fun initData() { private fun initData() {
App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, Observer { App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, Observer {
if (it.isEmpty()) {
getViewModelOfActivity(MainViewModel::class.java).initRss()
}
adapter.setItems(it) adapter.setItems(it)
}) })
} }

@ -1,6 +1,7 @@
package io.legado.app.ui.rss.read package io.legado.app.ui.rss.read
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
@ -15,6 +16,7 @@ import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.activity_rss_read.* import kotlinx.android.synthetic.main.activity_rss_read.*
import org.jetbrains.anko.toast
class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read), class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_read),
ReadRssViewModel.CallBack { ReadRssViewModel.CallBack {
@ -42,6 +44,9 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_rss_star -> viewModel.star() R.id.menu_rss_star -> viewModel.star()
R.id.menu_share_it -> viewModel.rssArticle?.let {
shareText("链接分享", it.link)
}
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
@ -115,4 +120,15 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
} }
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
private fun shareText(title: String, text: String) {
try {
val textIntent = Intent(Intent.ACTION_SEND)
textIntent.type = "text/plain"
textIntent.putExtra(Intent.EXTRA_TEXT, text)
startActivity(Intent.createChooser(textIntent, title))
} catch (e: Exception) {
toast(R.string.can_not_share)
}
}
} }

@ -78,17 +78,18 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
} }
fun clHtml(content: String): String { fun clHtml(content: String): String {
return """ return if (content.contains("<style>|style=".toRegex())) {
<html> content
<head> } else {
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> """
<style>img{max-width:100% !important; width:auto; height:auto;}</style> <style>
</head> img{max-width:100% !important; width:auto; height:auto;}
<body style:"height:auto;max-width: 100%; width:auto;"> video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
$content body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
</body></html> </style>$content
""" """
} }
}
interface CallBack { interface CallBack {
fun upStarMenu() fun upStarMenu()

@ -64,7 +64,8 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
execute { execute {
App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let { App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let {
val json = GSON.toJson(it) val json = GSON.toJson(it)
val file = FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json") val file =
FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json")
file.writeText(json) file.writeText(json)
} }
}.onSuccess { }.onSuccess {
@ -165,7 +166,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
else -> "格式不对" else -> "格式不对"
} }
}.onError { }.onError {
finally(it.localizedMessage) finally(it.localizedMessage ?: "")
}.onSuccess { }.onSuccess {
finally(it ?: "导入完成") finally(it ?: "导入完成")
} }

@ -1,86 +1,221 @@
package io.legado.app.ui.widget.font package io.legado.app.ui.widget.font
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.lib.dialogs.AlertBuilder import io.legado.app.constant.PreferKey
import io.legado.app.lib.dialogs.alert import io.legado.app.help.FileHelp
import io.legado.app.utils.applyTint import io.legado.app.help.permission.Permissions
import io.legado.app.utils.invisible import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.utils.visible import io.legado.app.utils.DocumentUtils
import kotlinx.android.synthetic.main.dialog_font_select.view.* import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefString
import io.legado.app.utils.toast
import kotlinx.android.synthetic.main.dialog_font_select.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import kotlin.coroutines.CoroutineContext
class FontSelectDialog(context: Context) : FontAdapter.CallBack { class FontSelectDialog : DialogFragment(),
Toolbar.OnMenuItemClickListener,
private val defaultFolder = CoroutineScope,
Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts" FontAdapter.CallBack {
lateinit var job: Job
private val fontFolderRequestCode = 35485
private lateinit var adapter: FontAdapter private lateinit var adapter: FontAdapter
private var builder: AlertBuilder<AlertDialog> private val fontFolder =
private var dialog: AlertDialog? = null App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator
@SuppressLint("InflateParams") override val coroutineContext: CoroutineContext
private var view: View = LayoutInflater.from(context).inflate(R.layout.dialog_font_select, null) get() = job + Main
var curPath: String? = null
var fontFolder: String? = null
var defaultFont: (() -> Unit)? = null
var selectFile: ((path: String) -> Unit)? = null
init { override fun onStart() {
builder = context.alert(title = context.getString(R.string.select_font)) { super.onStart()
customView = view val dm = DisplayMetrics()
positiveButton(R.string.default_font) { defaultFont?.invoke() } activity?.windowManager?.defaultDisplay?.getMetrics(dm)
negativeButton(R.string.cancel) dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
}
initData()
} }
fun show() { override fun onCreateView(
dialog = builder.show().applyTint() inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
job = Job()
return inflater.inflate(R.layout.dialog_font_select, container)
} }
private fun initData() = with(view) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = FontAdapter(context, this@FontSelectDialog) super.onViewCreated(view, savedInstanceState)
tool_bar.setTitle(R.string.select_font)
tool_bar.inflateMenu(R.menu.font_select)
tool_bar.setOnMenuItemClickListener(this)
adapter = FontAdapter(requireContext(), this)
recycler_view.layoutManager = LinearLayoutManager(context) recycler_view.layoutManager = LinearLayoutManager(context)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val files = getFontFiles()
if (files.isNullOrEmpty()) { val fontPath = getPrefString(PreferKey.fontFolder)
tv_no_data.visible() if (fontPath.isNullOrEmpty()) {
openFolder()
} else { } else {
tv_no_data.invisible() val uri = Uri.parse(fontPath)
adapter.setItems(files.toList()) if (DocumentFile.fromTreeUri(requireContext(), uri)?.canRead() == true) {
getFontFiles(uri)
} else {
openFolder()
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_default -> {
val pf = parentFragment
if (pf is CallBack) {
if ("" != pf.curFontPath) {
pf.selectFile("")
}
}
val activity = activity
if (activity is CallBack) {
if ("" != activity.curFontPath) {
activity.selectFile("")
}
}
dismiss()
}
R.id.menu_other -> {
openFolder()
}
}
return true
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private fun openFolder() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, fontFolderRequestCode)
} catch (e: java.lang.Exception) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted { getFontFilesOld() }
.request()
}
}
@SuppressLint("DefaultLocale")
private fun getFontFiles(uri: Uri) {
launch(IO) {
DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach { file ->
if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) {
DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let {
FileHelp.getFile(fontFolder + file.name).writeBytes(it)
}
}
}
try {
val file = File(fontFolder)
file.listFiles { pathName ->
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex())
}?.let {
withContext(Main) {
adapter.setItems(it.toList())
}
}
} catch (e: Exception) {
toast(e.localizedMessage ?: "")
}
} }
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun getFontFiles(): Array<File>? { private fun getFontFilesOld() {
val path = if (fontFolder.isNullOrEmpty()) { try {
defaultFolder val file =
} else fontFolder File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts")
return try {
val file = File(path)
file.listFiles { pathName -> file.listFiles { pathName ->
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex())
}?.let {
adapter.setItems(it.toList())
} }
} catch (e: Exception) { } catch (e: Exception) {
null toast(e.localizedMessage ?: "")
} }
} }
override fun onClick(file: File) { override fun onClick(file: File) {
file.absolutePath.let { file.absolutePath.let {
if (it != curPath) { val pf = parentFragment
selectFile?.invoke(it) if (pf is CallBack) {
dialog?.dismiss() if (it != pf.curFontPath) {
pf.selectFile(it)
}
}
val activity = activity
if (activity is CallBack) {
if (it != activity.curFontPath) {
activity.selectFile(it)
} }
} }
} }
dialog?.dismiss()
}
override fun curFilePath(): String { override fun curFilePath(): String {
return curPath ?: "" val pf = parentFragment
if (pf is CallBack) {
return pf.curFontPath
}
val activity = activity
if (activity is CallBack) {
return activity.curFontPath
}
return ""
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
fontFolderRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
putPrefString(PreferKey.fontFolder, uri.toString())
context?.contentResolver?.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
getFontFiles(uri)
}
}
}
}
interface CallBack {
fun selectFile(path: String)
val curFontPath: String
} }
} }

@ -0,0 +1,52 @@
package io.legado.app.utils
import android.content.Context
import android.net.Uri
object DocumentUtils {
@JvmStatic
fun writeText(context: Context, data: String, fileUri: Uri): Boolean {
return writeBytes(context, data.toByteArray(), fileUri)
}
@JvmStatic
fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean {
try {
context.contentResolver.openOutputStream(fileUri)?.let {
it.write(data)
it.close()
return true
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return false
}
@JvmStatic
fun readText(context: Context, uri: Uri): String? {
readBytes(context, uri)?.let {
return String(it)
}
return null
}
@JvmStatic
fun readBytes(context: Context, uri: Uri): ByteArray? {
try {
context.contentResolver.openInputStream(uri)?.let {
val len: Int = it.available()
val buffer = ByteArray(len)
it.read(buffer)
it.close()
return buffer
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17000000"
android:endColor="#00000000"
android:angle="90" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17FFFFFF"
android:endColor="#00FFFFFF"
android:angle="90" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17000000"
android:endColor="#00000000"
android:angle="270" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--实现应用背景颜色渐变-->
<gradient
android:startColor="#17FFFFFF"
android:endColor="#00FFFFFF"
android:angle="270" />
</shape>

@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

@ -1,20 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"> android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView <androidx.appcompat.widget.Toolbar
android:id="@+id/recycler_view" android:id="@+id/tool_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/tv_no_data" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent" />
android:padding="16dp"
android:text="@string/fonts_folder" />
</FrameLayout> </LinearLayout>

@ -2,6 +2,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_author"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_download"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>

@ -18,11 +18,9 @@
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:layout_margin="4dp" android:layout_margin="4dp"
android:background="@color/background" android:background="@color/btn_bg_press"
android:elevation="3dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical">
tools:ignore="UseCompoundDrawables">
<TextView <TextView
android:id="@+id/tv_name" android:id="@+id/tv_name"

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_download"
android:title="@string/action_download"
android:icon="@drawable/ic_play_24dp"
app:showAsAction="ifRoom" />
</menu>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_default"
android:title="@string/default_font"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_other"
android:title="@string/other_folder"
app:showAsAction="never" />
</menu>

@ -21,9 +21,9 @@
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_download_all" android:id="@+id/menu_download"
android:icon="@drawable/ic_download_line" android:icon="@drawable/ic_download_line"
android:title="@string/download_all" android:title="@string/download_offline"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item

@ -8,4 +8,10 @@
android:icon="@drawable/ic_star_border" android:icon="@drawable/ic_star_border"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/menu_share_it"
android:title="@string/share"
android:icon="@drawable/ic_menu_share"
app:showAsAction="ifRoom" />
</menu> </menu>

@ -13,9 +13,11 @@
<color name="transparent30">#30ffffff</color> <color name="transparent30">#30ffffff</color>
<color name="bg_divider_line">#363636</color> <color name="bg_divider_line">#363636</color>
<color name="btn_bg_press">#804D4D4D</color>
<color name="btn_bg_press_2">#80686868</color> <color name="btn_bg_press">#634D4D4D</color>
<color name="btn_bg_press_tp">#80C7C7C7</color> <color name="btn_bg_press_2">#63686868</color>
<color name="btn_bg_press_tp">#63C7C7C7</color>
<color name="darker_gray">#66666666</color> <color name="darker_gray">#66666666</color>
<color name="tv_btn_normal_black">#737373</color> <color name="tv_btn_normal_black">#737373</color>

@ -23,9 +23,9 @@
<color name="highlight">#d3321b</color> <color name="highlight">#d3321b</color>
<color name="btn_bg_press">#80ACACAC</color> <color name="btn_bg_press">#63ACACAC</color>
<color name="btn_bg_press_2">#80858585</color> <color name="btn_bg_press_2">#63858585</color>
<color name="btn_bg_press_tp">#802C2C2C</color> <color name="btn_bg_press_tp">#632C2C2C</color>
<color name="tv_btn_normal_black">#737373</color> <color name="tv_btn_normal_black">#737373</color>
<color name="tv_btn_press_black">#adadad</color> <color name="tv_btn_press_black">#adadad</color>

@ -45,7 +45,7 @@
<string name="receiving_shared_label">阅读·搜索</string> <string name="receiving_shared_label">阅读·搜索</string>
<string name="bookshelf_empty">书架还空着,先去添加吧!</string> <string name="bookshelf_empty">书架还空着,先去添加吧!</string>
<string name="action_search">搜索</string> <string name="action_search">搜索</string>
<string name="action_download">下载任务</string> <string name="action_download">下载</string>
<string name="layout_list">列表视图</string> <string name="layout_list">列表视图</string>
<string name="layout_grid3">网格视图三列</string> <string name="layout_grid3">网格视图三列</string>
<string name="layout_grid4">网格视图四列</string> <string name="layout_grid4">网格视图四列</string>
@ -96,6 +96,7 @@
<string name="pt_auto_download">自动下载最新章节</string> <string name="pt_auto_download">自动下载最新章节</string>
<string name="ps_auto_download">更新书籍时自动下载最新章节</string> <string name="ps_auto_download">更新书籍时自动下载最新章节</string>
<string name="backup_restore">备份与恢复</string> <string name="backup_restore">备份与恢复</string>
<string name="web_dav_set">WebDav设置</string>
<string name="web_dav_set_import_old">WebDav设置/导入旧版本数据</string> <string name="web_dav_set_import_old">WebDav设置/导入旧版本数据</string>
<string name="backup">备份</string> <string name="backup">备份</string>
<string name="restore">恢复</string> <string name="restore">恢复</string>
@ -208,6 +209,7 @@
<string name="download_start">开始下载</string> <string name="download_start">开始下载</string>
<string name="download_cancel">取消下载</string> <string name="download_cancel">取消下载</string>
<string name="no_download">暂无任务</string> <string name="no_download">暂无任务</string>
<string name="download_count">已下载 %d/%d</string>
<string name="import_select_book">导入选择书籍</string> <string name="import_select_book">导入选择书籍</string>
<string name="update_search_threads_num">更新和搜索线程数,如感觉卡顿请减小线程数,量力而行</string> <string name="update_search_threads_num">更新和搜索线程数,如感觉卡顿请减小线程数,量力而行</string>
<string name="change_icon">切换图标</string> <string name="change_icon">切换图标</string>
@ -576,5 +578,6 @@
<string name="no_group">未分组</string> <string name="no_group">未分组</string>
<string name="prev_sentence">上一句</string> <string name="prev_sentence">上一句</string>
<string name="next_sentence">下一句</string> <string name="next_sentence">下一句</string>
<string name="other_folder">其它目录</string>
</resources> </resources>

@ -2,6 +2,10 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<io.legado.app.lib.theme.prefs.ATEPreferenceCategory
android:title="@string/web_dav_set"
app:iconSpaceReserved="false">
<androidx.preference.EditTextPreference <androidx.preference.EditTextPreference
android:key="web_dav_url" android:key="web_dav_url"
android:title="WebDav 服务器地址" android:title="WebDav 服务器地址"
@ -20,6 +24,8 @@
android:summary="@string/web_dav_pw_s" android:summary="@string/web_dav_pw_s"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.lib.theme.prefs.ATEPreferenceCategory>
<io.legado.app.lib.theme.prefs.ATEPreferenceCategory app:iconSpaceReserved="false"> <io.legado.app.lib.theme.prefs.ATEPreferenceCategory app:iconSpaceReserved="false">
<androidx.preference.Preference <androidx.preference.Preference

Loading…
Cancel
Save