diff --git a/app/build.gradle b/app/build.gradle index d99118fc5..18d4ac8ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,7 @@ android { versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" project.ext.set("archivesBaseName", name + "_" + version) + multiDexEnabled true } buildTypes { release { @@ -98,7 +99,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.swiperefreshlayout:swiperefreshlayout: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' //lifecycle @@ -107,12 +108,12 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" //room - def room_version = '2.2.2' + def room_version = '2.2.3' implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" //paging - implementation 'androidx.paging:paging-runtime:2.1.0' + implementation 'androidx.paging:paging-runtime:2.1.1' //anko def anko_version = '0.10.8' diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 12171a213..2d01b7c58 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -4,8 +4,36 @@ * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】,提示存储权限,选择允许即可导入成功。 * 注意:由于安卓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的情景 + 订阅源的内容规则中存在` + ``` + +**2019/12/28** +* 添加下载界面 +* 添加分组备份 + **2019/12/23** * 修复每次打开翻页模式恢复默认的bug +* 修复m3u8报错问题 **2019/12/22** * 更新音频播放界面 diff --git a/app/src/main/java/io/legado/app/constant/Bus.kt b/app/src/main/java/io/legado/app/constant/Bus.kt index 395d51ec4..7795773e6 100644 --- a/app/src/main/java/io/legado/app/constant/Bus.kt +++ b/app/src/main/java/io/legado/app/constant/Bus.kt @@ -18,4 +18,6 @@ object Bus { const val AUDIO_SIZE = "audioSize" const val AUDIO_SPEED = "audioSpeed" const val SHOW_RSS = "showRss" + const val WEB_SERVICE_STOP = "webServiceStop" + const val UP_DOWNLOAD = "upDownload" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 4217e376d..0d8cad7cf 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -15,4 +15,7 @@ object PreferKey { const val cleanCache = "cleanCache" const val lastGroup = "lastGroup" const val pageAnim = "pageAnim" + const val readBookFont = "readBookFont" + const val fontFolder = "fontFolder" + const val backupPath = "backupUri" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt index 42e76ddd6..68de17a66 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt @@ -17,6 +17,9 @@ interface BookDao { @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") fun observeLocal(): LiveData> + @Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc") + fun observeDownload(): LiveData> + @Query("SELECT * FROM books WHERE `group` = :group") fun observeByGroup(group: Int): LiveData> diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt index 0078eb26b..c48bd6e6b 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt @@ -16,6 +16,9 @@ interface BookGroupDao { @get:Query("SELECT MAX(groupId) FROM book_groups") val maxId: Int + @Query("SELECT * FROM book_groups ORDER BY `order`") + fun all(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg bookGroup: BookGroup) diff --git a/app/src/main/java/io/legado/app/help/BookHelp.kt b/app/src/main/java/io/legado/app/help/BookHelp.kt index e6f17134a..1ea320505 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -35,13 +35,24 @@ object BookHelp { FileHelp.getFolder(getBookCachePath()) } + @Synchronized fun saveContent(book: Book, bookChapter: BookChapter, content: String) { 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 file = FileHelp.getFile(filePath) file.writeText(content) } + fun getChapterCount(book: Book): Int { + return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0 + } + fun hasContent(book: Book, bookChapter: BookChapter): Boolean { val filePath = getChapterPath(book, bookChapter) 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) + return "${getBookCachePath()}${File.separator}$bookFolder" + } + + private fun getChapterPath(book: Book, bookChapter: BookChapter): String { val chapterFile = 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 { diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 7062d1951..53d9a4279 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -1,9 +1,13 @@ 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.R import io.legado.app.help.FileHelp import io.legado.app.help.ReadBookConfig +import io.legado.app.utils.DocumentUtils import io.legado.app.utils.FileUtils import io.legado.app.utils.GSON import org.jetbrains.anko.defaultSharedPreferences @@ -27,8 +31,52 @@ object Backup { 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) + backupBookGroup(path) backupBookSource(path) backupRssSource(path) backupReplaceRule(path) @@ -39,7 +87,7 @@ object Backup { fun backup() { doAsync { - pbackup() + pBackup() uiThread { App.INSTANCE.toast(R.string.backup_success) } @@ -48,7 +96,7 @@ object Backup { fun autoBackup() { doAsync { - pbackup() + pBackup() } } @@ -56,13 +104,22 @@ object Backup { App.db.bookDao().allBooks.let { if (it.isNotEmpty()) { val json = GSON.toJson(it) - val file = FileHelp.getFile(path + File.separator + "bookshelf.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) { App.db.bookSourceDao().all.let { if (it.isNotEmpty()) { diff --git a/app/src/main/java/io/legado/app/help/storage/OldRule.kt b/app/src/main/java/io/legado/app/help/storage/OldRule.kt index 0c9baa659..14b4a9c53 100644 --- a/app/src/main/java/io/legado/app/help/storage/OldRule.kt +++ b/app/src/main/java/io/legado/app/help/storage/OldRule.kt @@ -205,4 +205,5 @@ object OldRule { return GSON.toJson(map) } + } diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index 53b040f28..920d61c0d 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -9,10 +9,7 @@ import com.jayway.jsonpath.ParseContext import io.legado.app.App import io.legado.app.R import io.legado.app.constant.AppConst -import io.legado.app.data.entities.Book -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.data.entities.* import io.legado.app.help.FileHelp import io.legado.app.help.ReadBookConfig import io.legado.app.utils.* @@ -47,6 +44,15 @@ object Restore { } catch (e: Exception) { e.printStackTrace() } + try { + val file = FileHelp.getFile(path + File.separator + "bookGroup.json") + val json = file.readText() + GSON.fromJsonArray(json)?.let { + App.db.bookGroupDao().insert(*it.toTypedArray()) + } + } catch (e: Exception) { + e.printStackTrace() + } try { val file = FileHelp.getFile(path + File.separator + "bookSource.json") val json = file.readText() @@ -105,47 +111,12 @@ object Restore { fun importYueDuData(context: Context) { GlobalScope.launch(IO) { try {// 导入书架 - val shelfFile = FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json") - val books = mutableListOf() - val items: List> = jsonPath.parse(shelfFile.readText()).read("$") - val existingBooks = App.db.bookDao().allBookUrls.toSet() - for (item in items) { - val jsonItem = jsonPath.parse(item) - val book = Book() - book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" - if (book.bookUrl.isBlank()) continue - book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" - if (book.bookUrl in existingBooks) { - Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") - continue - } - book.origin = jsonItem.readString("$.tag") ?: "" - book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" - book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" - book.type = - if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 - book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl - book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") - book.customCoverUrl = jsonItem.readString("$.customCoverPath") - book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 - book.canUpdate = jsonItem.readBool("$.allowUpdate") == true - book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 - book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 - book.durChapterTitle = jsonItem.readString("$.durChapterName") - book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 - book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 - book.group = jsonItem.readInt("$.group") ?: 0 - book.intro = jsonItem.readString("$.bookInfoBean.introduce") - book.latestChapterTitle = jsonItem.readString("$.lastChapterName") - book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 - book.order = jsonItem.readInt("$.serialNumber") ?: 0 - book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true - book.variable = jsonItem.readString("$.variable") - books.add(book) - } - App.db.bookDao().insert(*books.toTypedArray()) + val shelfFile = + FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json") + val json = shelfFile.readText() + val importCount = importOldBookshelf(json) withContext(Main) { - context.toast("成功导入书籍${books.size}") + context.toast("成功导入书籍${importCount}") } } catch (e: Exception) { withContext(Main) { @@ -156,17 +127,10 @@ object Restore { try {// Book source val sourceFile = FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json") - val bookSources = mutableListOf() - val items: List> = jsonPath.parse(sourceFile.readText()).read("$") - for (item in items) { - val jsonItem = jsonPath.parse(item) - OldRule.jsonToBookSource(jsonItem.jsonString())?.let { - bookSources.add(it) - } - } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + val json = sourceFile.readText() + val importCount = importOldSource(json) withContext(Main) { - context.toast("成功导入书源${bookSources.size}") + context.toast("成功导入书源${importCount}") } } catch (e: Exception) { withContext(Main) { @@ -177,26 +141,10 @@ object Restore { try {// Replace rules val ruleFile = FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json") - val replaceRules = mutableListOf() - val items: List> = jsonPath.parse(ruleFile.readText()).read("$") - val existingRules = App.db.replaceRuleDao().all.map { it.pattern }.toSet() - for ((index: Int, item: Map) in items.withIndex()) { - val jsonItem = jsonPath.parse(item) - val rule = ReplaceRule() - rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis().plus(index) - rule.pattern = jsonItem.readString("$.regex") ?: "" - if (rule.pattern.isEmpty() || rule.pattern in existingRules) continue - rule.name = jsonItem.readString("$.replaceSummary") ?: "" - rule.replacement = jsonItem.readString("$.replacement") ?: "" - rule.isRegex = jsonItem.readBool("$.isRegex") == true - rule.scope = jsonItem.readString("$.useTo") - rule.isEnabled = jsonItem.readBool("$.enable") == true - rule.order = jsonItem.readInt("$.serialNumber") ?: index - replaceRules.add(rule) - } - App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) + val json = ruleFile.readText() + val importCount = importOldReplaceRule(json) withContext(Main) { - context.toast("成功导入替换规则${replaceRules.size}") + context.toast("成功导入替换规则${importCount}") } } catch (e: Exception) { withContext(Main) { @@ -205,4 +153,81 @@ object Restore { } } } + + fun importOldBookshelf(json: String): Int { + val books = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + val existingBooks = App.db.bookDao().allBookUrls.toSet() + for (item in items) { + val jsonItem = jsonPath.parse(item) + val book = Book() + book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" + if (book.bookUrl.isBlank()) continue + book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" + if (book.bookUrl in existingBooks) { + Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") + continue + } + book.origin = jsonItem.readString("$.tag") ?: "" + book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" + book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" + book.type = + if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 + book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl + book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") + book.customCoverUrl = jsonItem.readString("$.customCoverPath") + book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 + book.canUpdate = jsonItem.readBool("$.allowUpdate") == true + book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 + book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 + book.durChapterTitle = jsonItem.readString("$.durChapterName") + book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 + book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 + book.group = jsonItem.readInt("$.group") ?: 0 + book.intro = jsonItem.readString("$.bookInfoBean.introduce") + book.latestChapterTitle = jsonItem.readString("$.lastChapterName") + book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 + book.order = jsonItem.readInt("$.serialNumber") ?: 0 + book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true + book.variable = jsonItem.readString("$.variable") + books.add(book) + } + App.db.bookDao().insert(*books.toTypedArray()) + return books.size + } + + fun importOldSource(json: String): Int { + val bookSources = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + for (item in items) { + val jsonItem = jsonPath.parse(item) + OldRule.jsonToBookSource(jsonItem.jsonString())?.let { + bookSources.add(it) + } + } + App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + return bookSources.size + } + + fun importOldReplaceRule(json: String): Int { + val replaceRules = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + val existingRules = App.db.replaceRuleDao().all.map { it.pattern }.toSet() + for ((index: Int, item: Map) in items.withIndex()) { + val jsonItem = jsonPath.parse(item) + val rule = ReplaceRule() + rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis().plus(index) + rule.pattern = jsonItem.readString("$.regex") ?: "" + if (rule.pattern.isEmpty() || rule.pattern in existingRules) continue + rule.name = jsonItem.readString("$.replaceSummary") ?: "" + rule.replacement = jsonItem.readString("$.replacement") ?: "" + rule.isRegex = jsonItem.readBool("$.isRegex") == true + rule.scope = jsonItem.readString("$.useTo") + rule.isEnabled = jsonItem.readBool("$.enable") == true + rule.order = jsonItem.readInt("$.serialNumber") ?: index + replaceRules.add(rule) + } + App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) + return replaceRules.size + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/Debug.kt b/app/src/main/java/io/legado/app/model/Debug.kt index fee62972e..7d7ec3627 100644 --- a/app/src/main/java/io/legado/app/model/Debug.kt +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -111,6 +111,24 @@ object Debug { log(webBook.sourceUrl, "⇒开始访问发现页:$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 -> { log(webBook.sourceUrl, "⇒开始搜索关键字:$key") searchDebug(webBook, key) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt index 28e022fb5..5475df744 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt @@ -16,7 +16,7 @@ object AnalyzeByRegex { // 新建容器 val info = arrayListOf() for (groupIndex in 0..resM.groupCount()) { - info.add(resM.group(groupIndex)) + info.add(resM.group(groupIndex)!!) } info } else { @@ -43,7 +43,7 @@ object AnalyzeByRegex { // 新建容器 val info = arrayListOf() for (groupIndex in 0..resM.groupCount()) { - info.add(resM.group(groupIndex)) + info.add(resM.group(groupIndex)!!) } books.add(info) } while (resM.find()) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index c2ed7c8da..3aa920237 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -320,7 +320,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { val pattern = Pattern.compile(rule.replaceRegex) val matcher = pattern.matcher(vResult) if (matcher.find()) { - matcher.group(0).replaceFirst(rule.replaceRegex.toRegex(), rule.replacement) + matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement) } else { "" } @@ -614,7 +614,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { val pattern = Pattern.compile("(第)(.+?)(章)") val matcher = pattern.matcher(s) 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 { s } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index bf8aded6d..b5343cf7f 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -117,7 +117,7 @@ class AnalyzeUrl( page?.let { val matcher = pagePattern.matcher(ruleUrl) while (matcher.find()) { - val pages = matcher.group(1).split(",") + val pages = matcher.group(1)!!.split(",") ruleUrl = if (page <= pages.size) { ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' }) } else { diff --git a/app/src/main/java/io/legado/app/model/rss/RssParser.kt b/app/src/main/java/io/legado/app/model/rss/RssParser.kt index 7606bd43e..816a3858f 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParser.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParser.kt @@ -122,9 +122,9 @@ object RssParser { if (matcherImg.find()) { val imgTag = matcherImg.group(1) val patternLink = "src\\s*=\\s*\"(.+?)\"".toPattern() - val matcherLink = patternLink.matcher(imgTag) + val matcherLink = patternLink.matcher(imgTag!!) if (matcherLink.find()) { - url = matcherLink.group(1).trim() + url = matcherLink.group(1)!!.trim() } } return url diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt index 2cedf7542..ef2638c29 100644 --- a/app/src/main/java/io/legado/app/service/AudioPlayService.kt +++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt @@ -26,6 +26,7 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp import io.legado.app.help.MediaHelp +import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.service.help.AudioPlay import io.legado.app.ui.audio.AudioPlayActivity @@ -121,8 +122,10 @@ class AudioPlayService : BaseService(), AudioPlay.status = Status.PLAY postEvent(Bus.AUDIO_STATE, Status.PLAY) mediaPlayer.reset() - val uri = Uri.parse(url) - mediaPlayer.setDataSource(this, uri, AudioPlay.headers()) + val analyzeUrl = + AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true) + val uri = Uri.parse(analyzeUrl.url) + mediaPlayer.setDataSource(this, uri, analyzeUrl.headerMap) mediaPlayer.prepareAsync() } catch (e: Exception) { launch { @@ -134,14 +137,18 @@ class AudioPlayService : BaseService(), } private fun pause(pause: Boolean) { - AudioPlayService.pause = pause - handler.removeCallbacks(mpRunnable) - position = mediaPlayer.currentPosition - mediaPlayer.pause() - upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) - AudioPlay.status = Status.PAUSE - postEvent(Bus.AUDIO_STATE, Status.PAUSE) - upNotification() + if (url.contains(".m3u8", false)) { + stopSelf() + } else { + AudioPlayService.pause = pause + handler.removeCallbacks(mpRunnable) + position = mediaPlayer.currentPosition + mediaPlayer.pause() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) + AudioPlay.status = Status.PAUSE + postEvent(Bus.AUDIO_STATE, Status.PAUSE) + upNotification() + } } private fun resume() { @@ -182,26 +189,24 @@ class AudioPlayService : BaseService(), */ override fun onPrepared(mp: MediaPlayer?) { if (pause) return - mp?.let { - mp.start() - mp.seekTo(position) - postEvent(Bus.AUDIO_SIZE, mp.duration) - bookChapter?.let { - it.end = mp.duration.toLong() - } - handler.removeCallbacks(mpRunnable) - handler.post(mpRunnable) + mediaPlayer.start() + mediaPlayer.seekTo(position) + postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration) + bookChapter?.let { + it.end = mediaPlayer.duration.toLong() } + handler.removeCallbacks(mpRunnable) + handler.post(mpRunnable) } /** * 播放出错 */ override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { - AudioPlay.status = Status.STOP - postEvent(Bus.AUDIO_STATE, Status.STOP) - launch { - toast("error: $what $extra $url") + if (!mediaPlayer.isPlaying) { + AudioPlay.status = Status.STOP + postEvent(Bus.AUDIO_STATE, Status.STOP) + launch { toast("error: $what $extra $url") } } return true } diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt index d665d6b8f..d8bfae260 100644 --- a/app/src/main/java/io/legado/app/service/DownloadService.kt +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -1,16 +1,19 @@ package io.legado.app.service import android.content.Intent +import android.os.Handler import androidx.core.app.NotificationCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.Action import io.legado.app.constant.AppConst +import io.legado.app.constant.Bus import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook +import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher import java.util.concurrent.Executors @@ -18,10 +21,26 @@ import java.util.concurrent.Executors class DownloadService : BaseService() { private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() private var tasks: ArrayList> = 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(this, Action.stop) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } override fun onCreate() { super.onCreate() - updateNotification("正在启动下载") + updateNotification(notificationContent) + handler.postDelayed(runnable, 1000) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -32,7 +51,7 @@ class DownloadService : BaseService() { intent.getIntExtra("start", 0), intent.getIntExtra("end", 0) ) - Action.stop -> stopSelf() + Action.stop -> stopDownload() } } return super.onStartCommand(intent, flags, startId) @@ -41,7 +60,9 @@ class DownloadService : BaseService() { override fun onDestroy() { tasks.clear() searchPool.close() + handler.removeCallbacks(runnable) super.onDestroy() + postEvent(Bus.UP_DOWNLOAD, false) } private fun download(bookUrl: String?, start: Int, end: Int) { @@ -55,7 +76,7 @@ class DownloadService : BaseService() { if (!BookHelp.hasContent(book, chapter)) { webBook.getContent(book, chapter, scope = this, context = searchPool) .onStart { - updateNotification(chapter.title) + notificationContent = chapter.title } .onSuccess(IO) { content -> 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) { - val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) - .setSmallIcon(R.drawable.ic_download) - .setOngoing(true) - .setContentTitle(getString(R.string.download_offline)) - .setContentText(content) - builder.addAction( - R.drawable.ic_stop_black_24dp, - getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, Action.stop) - ) - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + val builder = notificationBuilder + builder.setContentText(content) val notification = builder.build() startForeground(AppConst.notificationIdDownload, notification) } diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt index cdfd05581..977e8b0ac 100644 --- a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -45,16 +45,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener override fun onInit(status: Int) { launch { if (status == TextToSpeech.SUCCESS) { - val result = textToSpeech?.setLanguage(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()) - ttsIsSuccess = true - play() - } + textToSpeech?.language = Locale.CHINA + textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener()) + ttsIsSuccess = true + play() } else { toast(R.string.tts_init_failed) } diff --git a/app/src/main/java/io/legado/app/service/WebService.kt b/app/src/main/java/io/legado/app/service/WebService.kt index b32e6d6bc..25f2b6822 100644 --- a/app/src/main/java/io/legado/app/service/WebService.kt +++ b/app/src/main/java/io/legado/app/service/WebService.kt @@ -8,9 +8,11 @@ import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.Action import io.legado.app.constant.AppConst +import io.legado.app.constant.Bus import io.legado.app.help.IntentHelp import io.legado.app.utils.NetworkUtils import io.legado.app.utils.getPrefInt +import io.legado.app.utils.postEvent import io.legado.app.web.HttpServer import io.legado.app.web.WebSocketServer import kotlinx.coroutines.launch @@ -54,6 +56,7 @@ class WebService : BaseService() { if (webSocketServer?.isAlive == true) { webSocketServer?.stop() } + postEvent(Bus.WEB_SERVICE_STOP, true) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { diff --git a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt index 107be8809..19e950ec8 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt @@ -34,10 +34,10 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) { toast(R.string.copy_complete) } } - vw_zfb_hb.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/zfbhbrwm.png") } - vw_zfb_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/zfbskrwm.jpg") } - vw_wx_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/wxskrwm.jpg") } - vw_qq_rwm.setOnClickListener { openActionViewIntent("https://gedoor.github.io/MyBookshelf/qqskrwm.jpg") } + vw_zfb_hb.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png") } + vw_zfb_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg") } + vw_wx_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg") } + vw_qq_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg") } vw_zfb_hb_ssm.setOnClickListener { getZfbHb(this) } } @@ -75,9 +75,12 @@ class DonateActivity : BaseActivity(R.layout.activity_donate) { private fun aliDonate(context: Context) { try { - val qrCode = URLEncoder.encode("tsx06677nwdk3javroq4ef0", "utf-8") + val qrCode = URLEncoder.encode( + "https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other", + "utf-8" + ) 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)) context.startActivity(intent) } catch (e: Exception) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index fdc95233d..3a2523293 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -16,6 +16,7 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.Bus +import io.legado.app.constant.PreferKey import io.legado.app.constant.Status import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter @@ -203,6 +204,9 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.dispatchKeyEvent(event) } + /** + * 按键事件 + */ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> { @@ -217,11 +221,27 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } KeyEvent.KEYCODE_SPACE -> { 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) } + /** + * 长按事件 + */ override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_BACK -> { @@ -232,6 +252,9 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.onKeyLongPress(keyCode, event) } + /** + * 松开按键事件 + */ override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { @@ -257,6 +280,9 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.onKeyUp(keyCode, event) } + /** + * 音量键翻页 + */ private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { if (!read_menu.isVisible) { if (getPrefBoolean("volumeKeyPage", true)) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt index ea050dac8..3bc5060b0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -27,7 +27,7 @@ import org.jetbrains.anko.sdk27.listeners.onCheckedChange import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick -class ReadStyleDialog : DialogFragment() { +class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { override fun onStart() { super.onStart() @@ -86,17 +86,7 @@ class ReadStyleDialog : DialogFragment() { postEvent(Bus.UP_CONFIG, false) } tv_text_font.onClick { - FontSelectDialog(requireContext()).apply { - curPath = requireContext().getPrefString("readBookFont") - defaultFont = { - requireContext().putPrefString("readBookFont", "") - postEvent(Bus.UP_CONFIG, true) - } - selectFile = { - requireContext().putPrefString("readBookFont", it) - postEvent(Bus.UP_CONFIG, true) - } - }.show() + FontSelectDialog().show(childFragmentManager, "fontSelectDialog") } tv_text_indent.onClick { selector( @@ -270,4 +260,12 @@ class ReadStyleDialog : DialogFragment() { 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) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt index 41d4b937f..903cc3abc 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt @@ -81,7 +81,7 @@ class ContentView : FrameLayout { tv_bottom_right.setTextColor(it) } } - context.getPrefString("readBookFont")?.let { + context.getPrefString(PreferKey.readBookFont)?.let { if (it.isNotEmpty()) { content_text_view.typeface = Typeface.createFromFile(it) } else { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index 9d17e0f18..84dafcaa6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -9,7 +9,6 @@ import android.widget.FrameLayout import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig 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.utils.activity import io.legado.app.utils.getPrefInt @@ -26,7 +25,6 @@ class PageView(context: Context, attrs: AttributeSet) : var prevPage: ContentView? = null var curPage: ContentView? = null var nextPage: ContentView? = null - var curlView: CurlView? = null init { callBack = activity as? CallBack @@ -91,10 +89,6 @@ class PageView(context: Context, attrs: AttributeSet) : } fun upPageAnim(pageAnim: Int) { - if (curlView != null) { - removeView(curlView) - curlView = null - } pageDelegate = null pageDelegate = when (pageAnim) { 0 -> CoverPageDelegate(this) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlMesh.java b/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlMesh.java deleted file mode 100644 index 65ddbc363..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlMesh.java +++ /dev/null @@ -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 mArrDropShadowVertices; - private Array mArrIntersections; - private Array mArrOutputVertices; - private Array mArrRotatedVertices; - private Array mArrScanLines; - private Array mArrSelfShadowVertices; - private Array mArrTempShadowVertices; - private Array 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 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 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 getIntersections(Array 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 { - 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 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; - } - } -} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlPage.kt deleted file mode 100644 index 88f2fb16c..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlPage.kt +++ /dev/null @@ -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 - } - -} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlRenderer.kt b/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlRenderer.kt deleted file mode 100644 index 6d44ee1ee..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlRenderer.kt +++ /dev/null @@ -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 = 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 - } -} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlView.kt deleted file mode 100644 index ecd5d410d..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlView.kt +++ /dev/null @@ -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.

- *

- * 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 - } - -} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt index ef43b929e..ebb76100c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt @@ -39,11 +39,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie } } if (isMoved) { - isCancel = if (pageView.isScrollDelegate) { - if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 - } else { - if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 - } + isCancel = if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 isRunning = true //设置触摸点 setTouchPoint(e2.x, e2.y) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index c9e0ca546..3f78253b6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -69,7 +69,7 @@ abstract class PageDelegate(protected val pageView: PageView) { var isRunning = 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 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 touchY = y diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt index bf8a51a02..5b25392a2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -109,11 +109,7 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { curPage?.dispatchTouchEvent(e2) } if (isMoved) { - isCancel = if (pageView.isScrollDelegate) { - if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 - } else { - if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 - } + isCancel = if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 isRunning = true //设置触摸点 setTouchPoint(e2.x, e2.y) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt index eb58e616c..ac3f00283 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt @@ -1,126 +1,526 @@ package io.legado.app.ui.book.read.page.delegate -import android.graphics.Canvas -import android.view.MotionEvent +import android.graphics.* +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.curl.CurlPage -import io.legado.app.ui.book.read.page.curl.CurlView -import io.legado.app.utils.screenshot -import kotlin.math.abs +import kotlin.math.* -class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView), - CurlView.CallBack { +class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { - 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 { - pageView.curlView ?: let { - curlView = CurlView(pageView.context) - pageView.curlView = curlView - pageView.addView(curlView) - curlView?.mPageProvider = PageProvider() - curlView?.setSizeChangedObserver(SizeChangedObserver()) - curlView?.callBack = this - } + mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat() + mPaint.style = Paint.Style.FILL + //设置颜色数组 + createDrawable() + val cm = ColorMatrix() + val array = floatArrayOf( + 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 onTouch(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - curlView?.currentIndex = 1 - } + override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) { + super.setStartPoint(x, y, invalidate) + calcCornerXY(x, y) + } + + override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) { + super.setTouchPoint(x, y, invalidate) + //触摸y中间位置吧y变成屏幕高度 + //触摸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() { - } + 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() { + 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() - } else { - //如果不存在表示没有下一页了 - if (!hasNext()) { - noNext = true - return true - } - //下一页截图 - bitmap = nextPage?.screenshot() - } - isMoved = true + 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 { + calcPoints() + drawCurrentPageArea(canvas, it, mPath0) + drawNextPageAreaAndShadow(canvas, it) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, it) } } - if (isMoved) { - curlView?.canDraw = true - isCancel = if (pageView.isScrollDelegate) { - if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 + } + + /** + * 创建阴影的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 } + } + + /** + * 绘制翻起页背面 + */ + 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 { + left = (mBezierStart1.x - f3 - 1).toInt() + right = (mBezierStart1.x + 1).toInt() + mFolderShadowDrawable = mFolderShadowDrawableRL!! + } + canvas.save() + try { + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) } else { - if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 + canvas.clipPath(mPath1, Region.Op.INTERSECT) } - isRunning = true - //设置触摸点 - setTouchPoint(e2.x, e2.y) + } catch (ignored: Exception) { } - return isMoved + mPaint.colorFilter = mColorMatrixFilter + val dis = hypot( + mCornerX - mBezierControl1.x.toDouble(), + 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 onPageUp() { - curlView?.updatePages() - curlView?.requestRender() + /** + * 绘制翻起页的阴影 + */ + 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 { + 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() } - override fun pageChange(change: Int) { - pageView.post { - if (change > 0) { - pageView.moveToNextPage() + 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 { - pageView.moveToPrevPage() + 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() } - private inner class PageProvider : CurlView.PageProvider { + private fun drawCurrentPageArea( + 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 val pageCount: Int - get() = 3 + /** + * 计算拖拽点对应的拖拽脚 + */ + private fun calcCornerXY(x: Float, y: Float) { + 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) + } - 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) - 2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH) + private fun calcPoints() { + 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 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 } - // 定义书籍尺寸的变化监听器 - private inner class SizeChangedObserver : CurlView.SizeChangedObserver { - override fun onSizeChanged(width: Int, height: Int) { - curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) - } + /** + * 求解直线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 } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt index fa6ca7ea4..aeb83a093 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -91,7 +91,7 @@ class BookSourceEditActivity : GSON.toJson(getSource())?.let { sourceStr -> try { val textIntent = Intent(Intent.ACTION_SEND) - textIntent.setType("text/plain") + textIntent.type = "text/plain" textIntent.putExtra(Intent.EXTRA_TEXT, sourceStr) startActivity(Intent.createChooser(textIntent, "Source Share")) } catch (e: Exception) { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index 982476466..6ae3bd977 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -149,9 +149,11 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) importSource(file.readText(), finally) } else { withContext(Dispatchers.Main) { - finally("文件无法打开") + finally("打开文件出错") } } + }.onError { + finally(it.localizedMessage ?: "打开文件出错") } } @@ -194,7 +196,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) else -> "格式不对" } }.onError { - finally(it.localizedMessage) + finally(it.localizedMessage ?: "") }.onSuccess { finally(it ?: "导入完成") } diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt index 8a1990b88..c8860f021 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt @@ -13,6 +13,7 @@ import io.legado.app.R import io.legado.app.base.VMBaseFragment import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.lib.theme.backgroundColor import io.legado.app.utils.getViewModelOfActivity import kotlinx.android.synthetic.main.fragment_chapter_list.* import org.jetbrains.anko.sdk27.listeners.onClick @@ -58,6 +59,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme } private fun initView() { + ll_chapter_base_info.setBackgroundColor(backgroundColor) iv_chapter_top.onClick { recycler_view.scrollToPosition(0) } iv_chapter_bottom.onClick { if (adapter.itemCount > 0) { diff --git a/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt index 9eb761712..28f9c825d 100644 --- a/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt @@ -1,9 +1,13 @@ 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.Bundle import android.text.InputType import android.view.View +import androidx.documentfile.provider.DocumentFile import androidx.preference.EditTextPreference import androidx.preference.ListPreference 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.theme.ATH import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.DocumentUtils import io.legado.app.utils.LogUtils import io.legado.app.utils.applyTint 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?) { + job = Job() fun bindPreferenceSummaryToValue(preference: Preference?) { preference?.apply { onPreferenceChangeListener = this@WebDavConfigFragment @@ -64,6 +81,11 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference ATH.applyEdgeEffectColor(listView) } + override fun onDestroy() { + super.onDestroy() + job.cancel() + } + override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { when { preference?.key == "web_dav_password" -> if (newValue == null) { @@ -105,7 +127,18 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference WebDavHelp.showRestoreDialog(requireContext()) } .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 = "导入") { message = "是否导入旧版本数据" yesButton { @@ -122,12 +155,59 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference }.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) { - fun canRequestPackageInstalls() :Boolean { + fun canRequestPackageInstalls(): Boolean { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { return requireContext().packageManager.canRequestPackageInstalls() } @@ -143,8 +223,19 @@ class WebDavConfigFragment : PreferenceFragmentCompat(), Preference.OnPreference } }.show().applyTint() } else { - LogUtils.d("xxx","import old") + LogUtils.d("xxx", "import old") 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) + } + + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt b/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt index 2ae90c8a5..a2ff5d99e 100644 --- a/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt +++ b/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt @@ -1,25 +1,57 @@ package io.legado.app.ui.download import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.constant.Bus 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.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch class DownloadActivity : BaseActivity(R.layout.activity_download) { lateinit var adapter: DownloadAdapter private var bookshelfLiveData: LiveData>? = null + private var menu: Menu? = null override fun onActivityCreated(savedInstanceState: Bundle?) { initRecyclerView() 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() { recycler_view.layoutManager = LinearLayoutManager(this) adapter = DownloadAdapter(this) @@ -28,8 +60,22 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) { private fun initLiveData() { bookshelfLiveData?.removeObservers(this) + bookshelfLiveData = App.db.bookDao().observeDownload() bookshelfLiveData?.observe(this, Observer { adapter.setItems(it) }) } + + override fun observeLiveBus() { + observeEvent(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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt b/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt index 8407633ff..25b1b5b35 100644 --- a/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt @@ -1,18 +1,36 @@ package io.legado.app.ui.download import android.content.Context +import android.view.View import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.Book +import io.legado.app.help.BookHelp +import kotlinx.android.synthetic.main.item_download.view.* class DownloadAdapter(context: Context) : SimpleRecyclerAdapter(context, R.layout.item_download) { - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { + 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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index a059e380d..76874e612 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -1,8 +1,12 @@ package io.legado.app.ui.main +import android.app.Activity +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.KeyEvent import android.view.MenuItem +import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter @@ -13,7 +17,12 @@ import io.legado.app.BuildConfig import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.Bus +import io.legado.app.constant.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.Restore +import io.legado.app.help.storage.WebDavHelp import io.legado.app.lib.theme.ATH import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud @@ -28,7 +37,8 @@ import kotlinx.android.synthetic.main.activity_main.* class MainActivity : VMBaseActivity(R.layout.activity_main), BottomNavigationView.OnNavigationItemSelectedListener, ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() { - + private val backupSelectRequestCode = 4567489 + private val restoreSelectRequestCode = 654872 override val viewModel: MainViewModel get() = getViewModel(MainViewModel::class.java) @@ -137,6 +147,86 @@ class MainActivity : VMBaseActivity(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) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { @@ -153,5 +243,4 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 647b0bac2..27b37fe1a 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -4,7 +4,14 @@ import android.app.Application import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.constant.Bus +import io.legado.app.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.utils.GSON +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.fromJsonObject import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO 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(it).getAsync(url, mapOf()).await() + response.body()?.let { body -> + val sources = mutableListOf() + val items: List> = Restore.jsonPath.parse(body).read("$") + for (item in items) { + val jsonItem = Restore.jsonPath.parse(item) + GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> + sources.add(source) + } + } + App.db.rssSourceDao().insert(*sources.toTypedArray()) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt index 4b8d578ec..15a494694 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt @@ -19,6 +19,7 @@ import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor 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.getViewModel import io.legado.app.utils.putPrefInt @@ -62,7 +63,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b } R.id.menu_arrange_bookshelf -> { } - R.id.menu_download_all -> viewModel.downloadAll() + R.id.menu_download -> startActivity() } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 7f116694e..83d3c41a5 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -4,7 +4,6 @@ import android.app.Application import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.BookGroup -import io.legado.app.service.help.Download 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) - } - } - } } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt index c0d19851f..144fa4e19 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt @@ -9,7 +9,6 @@ import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.BookSource 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.utils.ACache 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) { with(holder.itemView) { if (payloads.isEmpty()) { - ATH.applyBackgroundTint(ll_title) tv_name.text = item.bookSourceName ll_title.onClick { val oldEx = exIndex diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt index b84538aa1..5b2a24114 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt @@ -7,14 +7,12 @@ import android.view.MenuItem import android.view.View import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment +import io.legado.app.constant.Bus 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.service.WebService 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.config.ConfigActivity 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.utils.LogUtils -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.startActivity -import io.legado.app.utils.toast +import io.legado.app.utils.* import kotlinx.android.synthetic.main.view_title_bar.* import org.jetbrains.anko.startActivity @@ -47,16 +43,14 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { override fun onCompatOptionsItemSelected(item: MenuItem) { when (item.itemId) { R.id.menu_help -> startActivity() - R.id.menu_backup -> PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { Backup.backup() } - .request() - R.id.menu_restore -> PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { WebDavHelp.showRestoreDialog(requireContext()) } - .request() + R.id.menu_backup -> { + val activity = activity as? MainActivity + activity?.backup() + } + R.id.menu_restore -> { + val activity = activity as? MainActivity + activity?.restore() + } } } @@ -64,7 +58,17 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { SharedPreferences.OnSharedPreferenceChangeListener { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + if (WebService.isRun) { + putPrefBoolean("webService", true) + } else { + putPrefBoolean("webService", false) + } addPreferencesFromResource(R.xml.pref_main) + observeEvent(Bus.WEB_SERVICE_STOP) { + findPreference("webService")?.let { + it.isChecked = false + } + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index 13ac93c20..0ec49f315 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -11,8 +11,10 @@ import io.legado.app.R import io.legado.app.base.BaseFragment import io.legado.app.data.entities.RssSource 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.source.manage.RssSourceActivity +import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.fragment_rss.* import kotlinx.android.synthetic.main.view_title_bar.* @@ -50,6 +52,9 @@ class RssFragment : BaseFragment(R.layout.fragment_rss), private fun initData() { App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, Observer { + if (it.isEmpty()) { + getViewModelOfActivity(MainViewModel::class.java).initRss() + } adapter.setItems(it) }) } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index 9fc29878b..037364296 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.rss.read import android.annotation.SuppressLint +import android.content.Intent import android.os.Bundle import android.view.KeyEvent 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.getViewModel import kotlinx.android.synthetic.main.activity_rss_read.* +import org.jetbrains.anko.toast class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_read), ReadRssViewModel.CallBack { @@ -42,6 +44,9 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_rss_star -> viewModel.star() + R.id.menu_share_it -> viewModel.rssArticle?.let { + shareText("链接分享", it.link) + } } return super.onCompatOptionsItemSelected(item) } @@ -115,4 +120,15 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r } 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt index 8c6f56279..98355d7d8 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -78,16 +78,17 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) { } fun clHtml(content: String): String { - return """ - - - - - - - $content - - """ + return if (content.contains("$content + """ + } } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt index 0e458b3aa..6ee254029 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt @@ -60,19 +60,20 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } } - fun exportSelection(ids: LinkedHashSet) { - execute { - App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let { - val json = GSON.toJson(it) - val file = FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json") - file.writeText(json) - } - }.onSuccess { - context.toast("成功导出至\n${Backup.exportPath}") - }.onError { - context.toast("导出失败\n${it.localizedMessage}") - } - } + fun exportSelection(ids: LinkedHashSet) { + execute { + App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let { + val json = GSON.toJson(it) + val file = + FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json") + file.writeText(json) + } + }.onSuccess { + context.toast("成功导出至\n${Backup.exportPath}") + }.onError { + context.toast("导出失败\n${it.localizedMessage}") + } + } fun addGroup(group: String) { execute { @@ -165,7 +166,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) else -> "格式不对" } }.onError { - finally(it.localizedMessage) + finally(it.localizedMessage ?: "") }.onSuccess { finally(it ?: "导入完成") } diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt index b276072b4..1fba2ce2e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt @@ -1,86 +1,221 @@ package io.legado.app.ui.widget.font 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.util.DisplayMetrics import android.view.LayoutInflater +import android.view.MenuItem 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 io.legado.app.App import io.legado.app.R -import io.legado.app.lib.dialogs.AlertBuilder -import io.legado.app.lib.dialogs.alert -import io.legado.app.utils.applyTint -import io.legado.app.utils.invisible -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.dialog_font_select.view.* +import io.legado.app.constant.PreferKey +import io.legado.app.help.FileHelp +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.utils.DocumentUtils +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 kotlin.coroutines.CoroutineContext -class FontSelectDialog(context: Context) : FontAdapter.CallBack { - - private val defaultFolder = - Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts" +class FontSelectDialog : DialogFragment(), + Toolbar.OnMenuItemClickListener, + CoroutineScope, + FontAdapter.CallBack { + lateinit var job: Job + private val fontFolderRequestCode = 35485 private lateinit var adapter: FontAdapter - private var builder: AlertBuilder - private var dialog: AlertDialog? = null - @SuppressLint("InflateParams") - private var view: View = LayoutInflater.from(context).inflate(R.layout.dialog_font_select, null) - var curPath: String? = null - var fontFolder: String? = null - var defaultFont: (() -> Unit)? = null - var selectFile: ((path: String) -> Unit)? = null + private val fontFolder = + App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator + override val coroutineContext: CoroutineContext + get() = job + Main - init { - builder = context.alert(title = context.getString(R.string.select_font)) { - customView = view - positiveButton(R.string.default_font) { defaultFont?.invoke() } - negativeButton(R.string.cancel) - } - initData() + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.windowManager?.defaultDisplay?.getMetrics(dm) + dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } - fun show() { - dialog = builder.show().applyTint() + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + job = Job() + return inflater.inflate(R.layout.dialog_font_select, container) } - private fun initData() = with(view) { - adapter = FontAdapter(context, this@FontSelectDialog) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + 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.adapter = adapter - val files = getFontFiles() - if (files.isNullOrEmpty()) { - tv_no_data.visible() + + val fontPath = getPrefString(PreferKey.fontFolder) + if (fontPath.isNullOrEmpty()) { + openFolder() } else { - tv_no_data.invisible() - adapter.setItems(files.toList()) + val uri = Uri.parse(fontPath) + 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(): Array? { - val path = if (fontFolder.isNullOrEmpty()) { - defaultFolder - } else fontFolder - return try { - val file = File(path) + 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") + private fun getFontFilesOld() { + try { + val file = + File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") file.listFiles { pathName -> pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) + }?.let { + adapter.setItems(it.toList()) } } catch (e: Exception) { - null + toast(e.localizedMessage ?: "") } } override fun onClick(file: File) { file.absolutePath.let { - if (it != curPath) { - selectFile?.invoke(it) - dialog?.dismiss() + val pf = parentFragment + if (pf is CallBack) { + 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 { - 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 } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt new file mode 100644 index 000000000..dcb128a8e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt @@ -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 + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_bottom.png b/app/src/main/res/drawable/bg_shadow_bottom.png deleted file mode 100644 index ac0124b67..000000000 Binary files a/app/src/main/res/drawable/bg_shadow_bottom.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_shadow_bottom.xml b/app/src/main/res/drawable/bg_shadow_bottom.xml new file mode 100644 index 000000000..68063fd27 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_bottom.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_bottom_night.png b/app/src/main/res/drawable/bg_shadow_bottom_night.png deleted file mode 100644 index c3036b036..000000000 Binary files a/app/src/main/res/drawable/bg_shadow_bottom_night.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_shadow_bottom_night.xml b/app/src/main/res/drawable/bg_shadow_bottom_night.xml new file mode 100644 index 000000000..85d10b753 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_bottom_night.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_top.png b/app/src/main/res/drawable/bg_shadow_top.png deleted file mode 100644 index cb8fee03d..000000000 Binary files a/app/src/main/res/drawable/bg_shadow_top.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_shadow_top.xml b/app/src/main/res/drawable/bg_shadow_top.xml new file mode 100644 index 000000000..98f1df214 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_top.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_top_night.png b/app/src/main/res/drawable/bg_shadow_top_night.png deleted file mode 100644 index 7984c35db..000000000 Binary files a/app/src/main/res/drawable/bg_shadow_top_night.png and /dev/null differ diff --git a/app/src/main/res/drawable/bg_shadow_top_night.xml b/app/src/main/res/drawable/bg_shadow_top_night.xml new file mode 100644 index 000000000..2ef90a767 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_top_night.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/side_nav_bar.xml b/app/src/main/res/drawable/side_nav_bar.xml deleted file mode 100644 index 6d81870b0..000000000 --- a/app/src/main/res/drawable/side_nav_bar.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_font_select.xml b/app/src/main/res/layout/dialog_font_select.xml index 08e33c0a9..60f080003 100644 --- a/app/src/main/res/layout/dialog_font_select.xml +++ b/app/src/main/res/layout/dialog_font_select.xml @@ -1,20 +1,18 @@ - - - + android:layout_height="match_parent" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml index d829e291c..f23794e94 100644 --- a/app/src/main/res/layout/item_download.xml +++ b/app/src/main/res/layout/item_download.xml @@ -2,6 +2,22 @@ + android:layout_height="wrap_content" + android:padding="16dp"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_find_book.xml b/app/src/main/res/layout/item_find_book.xml index 266e69e8d..8a95ed1e2 100644 --- a/app/src/main/res/layout/item_find_book.xml +++ b/app/src/main/res/layout/item_find_book.xml @@ -18,11 +18,9 @@ android:paddingTop="6dp" android:paddingBottom="6dp" android:layout_margin="4dp" - android:background="@color/background" - android:elevation="3dp" + android:background="@color/btn_bg_press" android:orientation="horizontal" - android:gravity="center_vertical" - tools:ignore="UseCompoundDrawables"> + android:gravity="center_vertical"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/font_select.xml b/app/src/main/res/menu/font_select.xml new file mode 100644 index 000000000..44a366ef8 --- /dev/null +++ b/app/src/main/res/menu/font_select.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_bookshelf.xml b/app/src/main/res/menu/main_bookshelf.xml index 45b586ca8..1c63a7629 100644 --- a/app/src/main/res/menu/main_bookshelf.xml +++ b/app/src/main/res/menu/main_bookshelf.xml @@ -21,9 +21,9 @@ app:showAsAction="never" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index f9c4c4f7c..4933cfa2b 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -13,9 +13,11 @@ #30ffffff #363636 - #804D4D4D - #80686868 - #80C7C7C7 + + #634D4D4D + #63686868 + #63C7C7C7 + #66666666 #737373 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index da1e3c9b0..d150286a4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -23,9 +23,9 @@ #d3321b - #80ACACAC - #80858585 - #802C2C2C + #63ACACAC + #63858585 + #632C2C2C #737373 #adadad diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6231a701f..5036723df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,7 +45,7 @@ 阅读·搜索 书架还空着,先去添加吧! 搜索 - 下载任务 + 下载 列表视图 网格视图三列 网格视图四列 @@ -96,6 +96,7 @@ 自动下载最新章节 更新书籍时自动下载最新章节 备份与恢复 + WebDav设置 WebDav设置/导入旧版本数据 备份 恢复 @@ -208,6 +209,7 @@ 开始下载 取消下载 暂无任务 + 已下载 %d/%d 导入选择书籍 更新和搜索线程数,如感觉卡顿请减小线程数,量力而行 切换图标 @@ -576,5 +578,6 @@ 未分组 上一句 下一句 + 其它目录 diff --git a/app/src/main/res/xml/pref_config_web_dav.xml b/app/src/main/res/xml/pref_config_web_dav.xml index 42733f458..0b4a84e09 100644 --- a/app/src/main/res/xml/pref_config_web_dav.xml +++ b/app/src/main/res/xml/pref_config_web_dav.xml @@ -2,23 +2,29 @@ - - - - - + + + + + + + + +