Merge pull request #3 from gedoor/master

Sys Code
pull/921/head
ag2s20150909 4 years ago committed by GitHub
commit e6010b2b03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/src/main/AndroidManifest.xml
  2. 4
      app/src/main/assets/updateLog.md
  3. 4
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  4. 4
      app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt
  5. 38
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  6. 28
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  7. 56
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  8. 106
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  9. 30
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  10. 57
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  11. 27
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  12. 90
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  13. 52
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt
  14. 50
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt
  15. 4
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  16. 25
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt
  17. 54
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  18. 98
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  19. 25
      app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt
  20. 34
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListActivity.kt
  21. 20
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt
  22. 11
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListViewModel.kt
  23. 26
      app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt
  24. 173
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  25. 195
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  26. 31
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  27. 52
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  28. 43
      app/src/main/java/io/legado/app/ui/document/FilePicker.kt
  29. 137
      app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt
  30. 52
      app/src/main/java/io/legado/app/ui/document/FilePickerDialog.kt
  31. 8
      app/src/main/java/io/legado/app/ui/document/adapter/FileAdapter.kt
  32. 6
      app/src/main/java/io/legado/app/ui/document/adapter/PathAdapter.kt
  33. 2
      app/src/main/java/io/legado/app/ui/document/entity/FileItem.kt
  34. 2
      app/src/main/java/io/legado/app/ui/document/entity/JavaBean.kt
  35. 2
      app/src/main/java/io/legado/app/ui/document/utils/ConvertUtils.kt
  36. 2
      app/src/main/java/io/legado/app/ui/document/utils/FilePickerIcon.java
  37. 284
      app/src/main/java/io/legado/app/ui/filepicker/FilePicker.kt
  38. 35
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  39. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  40. 10
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  41. 23
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt
  42. 106
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  43. 58
      app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt
  44. 32
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  45. 25
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  46. 101
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  47. 78
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  48. 2
      app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt
  49. 10
      app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt
  50. 9
      app/src/main/java/io/legado/app/utils/ActivityExtensions.kt
  51. 2
      app/src/main/java/io/legado/app/utils/FileUtils.kt
  52. 11
      app/src/main/java/io/legado/app/utils/FragmentExtensions.kt
  53. 24
      app/src/main/java/io/legado/app/utils/UriExtensions.kt
  54. 2
      app/src/main/res/layout/activity_chapter_list.xml
  55. 2
      app/src/main/res/layout/activity_rss_artivles.xml
  56. 3
      app/src/main/res/layout/dialog_toc_regex.xml
  57. 10
      app/src/main/res/menu/book_source.xml
  58. 10
      app/src/main/res/menu/replace_rule.xml
  59. 10
      app/src/main/res/menu/rss_source.xml
  60. 15
      app/src/main/res/menu/speak_engine.xml
  61. 4
      app/src/main/res/values-zh-rHK/strings.xml
  62. 4
      app/src/main/res/values-zh-rTW/strings.xml
  63. 4
      app/src/main/res/values-zh/strings.xml
  64. 4
      app/src/main/res/values/strings.xml
  65. 2
      build.gradle
  66. 26
      epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java
  67. 8
      epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java
  68. 15
      epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java
  69. 323
      epublib/src/main/java/me/ag2s/epublib/domain/Book.java
  70. 320
      epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java
  71. 4
      epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java
  72. 9
      epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java
  73. 46
      epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java
  74. 30
      epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java
  75. 19
      epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java
  76. 14
      epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java
  77. 29
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java
  78. 63
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java
  79. 36
      epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java

@ -294,6 +294,12 @@
android:name=".ui.about.ReadRecordActivity" android:name=".ui.about.ReadRecordActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" /> android:hardwareAccelerated="true" />
<!-- 选择文件 -->
<activity
android:name=".ui.document.FilePickerActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme.Transparent" />
<!-- 文字处理 --> <!-- 文字处理 -->
<activity <activity
android:name=".receiver.SharedReceiverActivity" android:name=".receiver.SharedReceiverActivity"

@ -3,8 +3,10 @@
* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2021/03/26** **2021/03/31**
* 优化epubLib by ag2s20150909 * 优化epubLib by ag2s20150909
* 升级库,修改弃用方法
* tts引擎添加导入导出功能
**2021/03/23** **2021/03/23**
* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 * 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右

@ -154,7 +154,9 @@ abstract class BaseActivity<VB : ViewBinding>(
} }
if (AppConfig.isGooglePlay) { if (AppConfig.isGooglePlay) {
ThemeConfig.getBgImage(this)?.let { ThemeConfig.getBgImage(this)?.let {
window.decorView.background = it kotlin.runCatching {
window.decorView.background = it
}
} }
} }
} }

@ -13,7 +13,7 @@ import io.legado.app.utils.toastOnUi
class PermissionActivity : AppCompatActivity() { class PermissionActivity : AppCompatActivity() {
private val startSettingActivity = private val settingActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
RequestPlugins.sRequestCallback?.onSettingActivityResult() RequestPlugins.sRequestCallback?.onSettingActivityResult()
finish() finish()
@ -37,7 +37,7 @@ class PermissionActivity : AppCompatActivity() {
-> try { -> try {
val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
settingIntent.data = Uri.fromParts("package", packageName, null) settingIntent.data = Uri.fromParts("package", packageName, null)
startSettingActivity.launch(settingIntent) settingActivityResult.launch(settingIntent)
} catch (e: Exception) { } catch (e: Exception) {
toastOnUi(R.string.tip_cannot_jump_setting_page) toastOnUi(R.string.tip_cannot_jump_setting_page)
finish() finish()

@ -62,19 +62,19 @@ class EpubFile(var book: Book) {
} }
} }
private var epubBook: EpubBook? = null
private var mCharset: Charset = Charset.defaultCharset() private var mCharset: Charset = Charset.defaultCharset()
private var epubBook: EpubBook? = null
get() {
if (field != null) {
return field
}
field = readEpub()
return field
}
init { init {
try { try {
val inputStream = if (book.bookUrl.isContentScheme()) { epubBook?.let {
val uri = Uri.parse(book.bookUrl)
appCtx.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
epubBook = readEpub(inputStream)
if (epubBook != null) {
if (book.coverUrl.isNullOrEmpty()) { if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath( book.coverUrl = FileUtils.getPath(
appCtx.externalFilesDir, appCtx.externalFilesDir,
@ -84,8 +84,8 @@ class EpubFile(var book: Book) {
} }
if (!File(book.coverUrl!!).exists()) { if (!File(book.coverUrl!!).exists()) {
/*部分书籍DRM处理后,封面获取异常,待优化*/ /*部分书籍DRM处理后,封面获取异常,待优化*/
epubBook!!.coverImage?.inputStream?.use { it.coverImage?.inputStream?.use { input ->
val cover = BitmapFactory.decodeStream(it) val cover = BitmapFactory.decodeStream(input)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out) cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush() out.flush()
@ -99,9 +99,15 @@ class EpubFile(var book: Book) {
} }
/*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/ /*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/
private fun readEpub(input: InputStream?): EpubBook? { private fun readEpub(): EpubBook? {
if (input == null) return null
try { try {
val input = if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl)
appCtx.contentResolver.openInputStream(uri)
} else {
File(book.bookUrl).inputStream()
}
input ?: return null
val inZip = ZipInputStream(input) val inZip = ZipInputStream(input)
var zipEntry: ZipEntry? var zipEntry: ZipEntry?
val resources = Resources() val resources = Resources()
@ -120,7 +126,7 @@ class EpubFile(var book: Book) {
} }
resources.add(resource) resources.add(resource)
} while (zipEntry != null) } while (zipEntry != null)
if (resources.size() > 0) return EpubReader().readEpubBook(resources) if (resources.size() > 0) return EpubReader().readEpub(resources)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -129,9 +135,7 @@ class EpubFile(var book: Book) {
private fun getContent(chapter: BookChapter): String? { private fun getContent(chapter: BookChapter): String? {
/*获取当前章节文本*/ /*获取当前章节文本*/
val string = getChildChapter(chapter, chapter.url) return getChildChapter(chapter, chapter.url)
return string
} }
private fun getChildChapter(chapter: BookChapter, href: String): String? { private fun getChildChapter(chapter: BookChapter, href: String): String? {

@ -1,7 +1,6 @@
package io.legado.app.ui.audio package io.legado.app.ui.audio
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.icu.text.SimpleDateFormat import android.icu.text.SimpleDateFormat
import android.os.Build import android.os.Build
@ -25,7 +24,7 @@ import io.legado.app.help.ImageLoader
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import io.legado.app.ui.book.changesource.ChangeSourceDialog import io.legado.app.ui.book.changesource.ChangeSourceDialog
import io.legado.app.ui.book.toc.ChapterListActivity import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.* import io.legado.app.utils.*
@ -42,7 +41,6 @@ class AudioPlayActivity :
override val viewModel: AudioPlayViewModel override val viewModel: AudioPlayViewModel
by viewModels() by viewModels()
private var requestCodeChapter = 8461
private var adjustProgress = false private var adjustProgress = false
private val progressTimeFormat by lazy { private val progressTimeFormat by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -51,6 +49,13 @@ class AudioPlayActivity :
java.text.SimpleDateFormat("mm:ss", Locale.getDefault()) java.text.SimpleDateFormat("mm:ss", Locale.getDefault())
} }
} }
private val tocActivityResult = registerForActivityResult(TocActivityResult()) {
it?.let {
if (it.first != AudioPlay.durChapterIndex) {
AudioPlay.skipTo(this, it.first)
}
}
}
override fun getViewBinding(): ActivityAudioPlayBinding { override fun getViewBinding(): ActivityAudioPlayBinding {
return ActivityAudioPlayBinding.inflate(layoutInflater) return ActivityAudioPlayBinding.inflate(layoutInflater)
@ -107,9 +112,7 @@ class AudioPlayActivity :
}) })
binding.ivChapter.setOnClickListener { binding.ivChapter.setOnClickListener {
AudioPlay.book?.let { AudioPlay.book?.let {
startActivityForResult<ChapterListActivity>(requestCodeChapter) { tocActivityResult.launch(it.bookUrl)
putExtra("bookUrl", it.bookUrl)
}
} }
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
@ -183,19 +186,6 @@ class AudioPlayActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
requestCodeChapter -> data?.getIntExtra("index", AudioPlay.durChapterIndex)?.let {
if (it != AudioPlay.durChapterIndex) {
AudioPlay.skipTo(this, it)
}
}
}
}
}
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<Boolean>(EventBus.MEDIA_BUTTON) { observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
if (it) { if (it) {

@ -1,6 +1,5 @@
package io.legado.app.ui.book.cache package io.legado.app.ui.book.cache
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -25,8 +24,8 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.service.help.CacheBook import io.legado.app.service.help.CacheBook
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.widget.dialog.TextListDialog import io.legado.app.ui.widget.dialog.TextListDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,11 +35,25 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>(), class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>(),
FilePickerDialog.CallBack,
CacheAdapter.CallBack { CacheAdapter.CallBack {
private val exportRequestCode = 32
private val exportDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
ACache.get(this@CacheActivity).put(exportBookPathKey, uri.toString())
startExport(uri.toString())
} else {
uri.path?.let { path ->
ACache.get(this@CacheActivity).put(exportBookPathKey, path)
startExport(path)
}
}
}
private val exportBookPathKey = "exportBookPath" private val exportBookPathKey = "exportBookPath"
lateinit var adapter: CacheAdapter lateinit var adapter: CacheAdapter
private var groupLiveData: LiveData<List<BookGroup>>? = null private var groupLiveData: LiveData<List<BookGroup>>? = null
@ -215,9 +228,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
if (!path.isNullOrEmpty()) { if (!path.isNullOrEmpty()) {
default.add(path) default.add(path)
} }
FilePicker.selectFolder(this, exportRequestCode, otherActions = default) { exportDir.launch(
startExport(it) FilePickerParam(
} otherActions = default.toTypedArray()
)
)
} }
private fun startExport(path: String) { private fun startExport(path: String) {
@ -244,27 +259,4 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}.show() }.show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
exportRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
ACache.get(this@CacheActivity).put(exportBookPathKey, uri.toString())
startExport(uri.toString())
} else {
uri.path?.let { path ->
ACache.get(this@CacheActivity).put(exportBookPathKey, path)
startExport(path)
}
}
}
}
}
}
} }

@ -1,7 +1,6 @@
package io.legado.app.ui.book.info package io.legado.app.ui.book.info
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
@ -9,6 +8,7 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
@ -35,7 +35,7 @@ import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.ChapterListActivity import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
@ -47,9 +47,37 @@ class BookInfoActivity :
ChangeSourceDialog.CallBack, ChangeSourceDialog.CallBack,
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val requestCodeChapterList = 568 private val tocActivityResult = registerForActivityResult(TocActivityResult()) {
private val requestCodeInfoEdit = 562 it?.let {
private val requestCodeRead = 432 viewModel.bookData.value?.let { book ->
if (book.durChapterIndex != it.first) {
book.durChapterIndex = it.first
book.durChapterPos = it.second
}
startReadActivity(book)
}
} ?: let {
if (!viewModel.inBookshelf) {
viewModel.delBook()
}
}
}
private val readBookResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK) {
viewModel.inBookshelf = true
upTvBookshelf()
}
}
private val infoEditResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK) {
viewModel.upEditBook()
}
}
override val viewModel: BookInfoViewModel by viewModels() override val viewModel: BookInfoViewModel by viewModels()
@ -82,11 +110,10 @@ class BookInfoActivity :
R.id.menu_edit -> { R.id.menu_edit -> {
if (viewModel.inBookshelf) { if (viewModel.inBookshelf) {
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
startActivityForResult<BookInfoEditActivity>( infoEditResult.launch(
requestCodeInfoEdit Intent(this, BookInfoEditActivity::class.java)
) { .putExtra("bookUrl", it.bookUrl)
putExtra("bookUrl", it.bookUrl) )
}
} }
} else { } else {
toastOnUi(R.string.after_add_bookshelf) toastOnUi(R.string.after_add_bookshelf)
@ -303,11 +330,7 @@ class BookInfoActivity :
return return
} }
viewModel.bookData.value?.let { viewModel.bookData.value?.let {
startActivityForResult<ChapterListActivity>( tocActivityResult.launch(it.bookUrl)
requestCodeChapterList
) {
putExtra("bookUrl", it.bookUrl)
}
} }
} }
@ -327,19 +350,17 @@ class BookInfoActivity :
private fun startReadActivity(book: Book) { private fun startReadActivity(book: Book) {
when (book.type) { when (book.type) {
BookType.audio -> startActivityForResult<AudioPlayActivity>( BookType.audio -> readBookResult.launch(
requestCodeRead Intent(this, AudioPlayActivity::class.java)
) { .putExtra("bookUrl", book.bookUrl)
putExtra("bookUrl", book.bookUrl) .putExtra("inBookshelf", viewModel.inBookshelf)
putExtra("inBookshelf", viewModel.inBookshelf) )
} else -> readBookResult.launch(
else -> startActivityForResult<ReadBookActivity>( Intent(this, ReadBookActivity::class.java)
requestCodeRead .putExtra("bookUrl", book.bookUrl)
) { .putExtra("inBookshelf", viewModel.inBookshelf)
putExtra("bookUrl", book.bookUrl) .putExtra("key", IntentDataHelp.putData(book))
putExtra("inBookshelf", viewModel.inBookshelf) )
putExtra("key", IntentDataHelp.putData(book))
}
} }
} }
@ -381,33 +402,4 @@ class BookInfoActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeInfoEdit ->
if (resultCode == Activity.RESULT_OK) {
viewModel.upEditBook()
}
requestCodeChapterList ->
if (resultCode == Activity.RESULT_OK) {
viewModel.bookData.value?.let {
data?.getIntExtra("index", it.durChapterIndex)?.let { index ->
if (it.durChapterIndex != index) {
it.durChapterIndex = index
it.durChapterPos = 0
}
startReadActivity(it)
}
}
} else {
if (!viewModel.inBookshelf) {
viewModel.delBook()
}
}
requestCodeRead -> if (resultCode == Activity.RESULT_OK) {
viewModel.inBookshelf = true
upTvBookshelf()
}
}
}
} }

@ -1,11 +1,11 @@
package io.legado.app.ui.book.info.edit package io.legado.app.ui.book.info.edit
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.R import io.legado.app.R
@ -22,7 +22,12 @@ class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(), VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val resultSelectCover = 132 private val selectCoverResult =
registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
coverChangeTo(uri)
}
}
override val viewModel: BookInfoEditViewModel override val viewModel: BookInfoEditViewModel
by viewModels() by viewModels()
@ -60,7 +65,7 @@ class BookInfoEditActivity :
} }
} }
tvSelectCover.setOnClickListener { tvSelectCover.setOnClickListener {
selectImage() selectCoverResult.launch("image/*")
} }
tvRefreshCover.setOnClickListener { tvRefreshCover.setOnClickListener {
viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString()
@ -96,13 +101,6 @@ class BookInfoEditActivity :
} }
} }
private fun selectImage() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, resultSelectCover)
}
override fun coverChangeTo(coverUrl: String) { override fun coverChangeTo(coverUrl: String) {
viewModel.book?.customCoverUrl = coverUrl viewModel.book?.customCoverUrl = coverUrl
binding.tieCoverUrl.setText(coverUrl) binding.tieCoverUrl.setText(coverUrl)
@ -144,16 +142,4 @@ class BookInfoEditActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
resultSelectCover -> {
if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
coverChangeTo(uri)
}
}
}
}
}
} }

@ -1,6 +1,5 @@
package io.legado.app.ui.book.local package io.legado.app.ui.book.local
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -21,8 +20,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.lib.permission.Permissions import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -36,17 +34,31 @@ import java.util.*
* 导入本地书籍界面 * 导入本地书籍界面
*/ */
class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookViewModel>(), class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookViewModel>(),
FilePickerDialog.CallBack,
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
SelectActionBar.CallBack, ImportBookAdapter.CallBack,
ImportBookAdapter.CallBack { SelectActionBar.CallBack {
private val requestCodeSelectFolder = 342
private var rootDoc: DocumentFile? = null private var rootDoc: DocumentFile? = null
private val subDocs = arrayListOf<DocumentFile>() private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null private var localUriLiveData: LiveData<List<String>>? = null
private var sdPath = FileUtils.getSdCardPath() private var sdPath = FileUtils.getSdCardPath()
private var path = sdPath private var path = sdPath
private val selectFolder = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.importBookPath = uri.toString()
initRootDoc()
} else {
uri.path?.let { path ->
AppConfig.importBookPath = path
initRootDoc()
}
}
}
override val viewModel: ImportBookViewModel override val viewModel: ImportBookViewModel
by viewModels() by viewModels()
@ -69,7 +81,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_select_folder -> FilePicker.selectFolder(this, requestCodeSelectFolder) R.id.menu_select_folder -> selectFolder.launch(null)
R.id.menu_scan_folder -> scanFolder() R.id.menu_scan_folder -> scanFolder()
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
@ -129,14 +141,14 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
when { when {
lastPath.isNullOrEmpty() -> { lastPath.isNullOrEmpty() -> {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder) selectFolder.launch(null)
} }
lastPath.isContentScheme() -> { lastPath.isContentScheme() -> {
val rootUri = Uri.parse(lastPath) val rootUri = Uri.parse(lastPath)
rootDoc = DocumentFile.fromTreeUri(this, rootUri) rootDoc = DocumentFile.fromTreeUri(this, rootUri)
if (rootDoc == null) { if (rootDoc == null) {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder) selectFolder.launch(null)
} else { } else {
subDocs.clear() subDocs.clear()
upPath() upPath()
@ -144,7 +156,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
} }
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> { Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder) selectFolder.launch(null)
} }
else -> { else -> {
binding.tvEmptyMsg.visible() binding.tvEmptyMsg.visible()
@ -274,29 +286,6 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeSelectFolder -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.importBookPath = uri.toString()
initRootDoc()
} else {
uri.path?.let { path ->
AppConfig.importBookPath = path
initRootDoc()
}
}
}
}
}
}
@Synchronized @Synchronized
override fun nextDoc(uri: Uri) { override fun nextDoc(uri: Uri) {
if (uri.toString().isContentScheme()) { if (uri.toString().isContentScheme()) {

@ -46,7 +46,7 @@ import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.searchContent.SearchContentActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.ChapterListActivity import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.login.SourceLogin import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity import io.legado.app.ui.replace.edit.ReplaceEditActivity
@ -71,23 +71,26 @@ class ReadBookActivity : ReadBookBaseActivity(),
ColorPickerDialogListener { ColorPickerDialogListener {
private val tocActivity = private val tocActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(TocActivityResult()) {
it.data?.let { data -> it?.let {
data.getIntExtra("index", ReadBook.durChapterIndex).let { index -> if (it.first != ReadBook.durChapterIndex) {
if (index != ReadBook.durChapterIndex) { viewModel.openChapter(it.first, it.second)
val chapterPos = data.getIntExtra("chapterPos", 0)
viewModel.openChapter(index, chapterPos)
}
} }
} }
} }
private val sourceEditActivity = private val sourceEditActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
upView() if (it.resultCode == RESULT_OK) {
viewModel.upBookSource {
upView()
}
}
} }
private val replaceActivity = private val replaceActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
viewModel.replaceRuleChanged() if (it.resultCode == RESULT_OK) {
viewModel.replaceRuleChanged()
}
} }
private val searchContentActivity = private val searchContentActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -727,9 +730,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
*/ */
override fun openChapterList() { override fun openChapterList() {
ReadBook.book?.let { ReadBook.book?.let {
tocActivity.launch(Intent(this, ChapterListActivity::class.java).apply { tocActivity.launch(it.bookUrl)
putExtra("bookUrl", it.bookUrl)
})
} }
} }

@ -1,13 +1,12 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
@ -24,15 +23,15 @@ import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.getSecondaryTextColor import io.legado.app.lib.theme.getSecondaryTextColor
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray import rxhttp.wrapper.param.toByteArray
import java.io.File import java.io.File
class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack { class BgTextConfigDialog : BaseDialogFragment() {
companion object { companion object {
const val TEXT_COLOR = 121 const val TEXT_COLOR = 121
@ -40,13 +39,27 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} }
private val binding by viewBinding(DialogReadBgTextBinding::bind) private val binding by viewBinding(DialogReadBgTextBinding::bind)
private val requestCodeBg = 123
private val requestCodeExport = 131
private val requestCodeImport = 132
private val configFileName = "readConfig.zip" private val configFileName = "readConfig.zip"
private lateinit var adapter: BgAdapter private lateinit var adapter: BgAdapter
private var primaryTextColor = 0 private var primaryTextColor = 0
private var secondaryTextColor = 0 private var secondaryTextColor = 0
private val importFormNet = "网络导入"
private val selectBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
setBgFromUri(it)
}
private val selectExportDir = registerForActivityResult(FilePicker()) {
it?.let {
exportConfig(it)
}
}
private val selectImportDoc = registerForActivityResult(FilePicker()) {
it ?: return@registerForActivityResult
if (it.toString() == importFormNet) {
importNetConfigAlert()
} else {
importConfig(it)
}
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -108,7 +121,9 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
tvName.text = getString(R.string.select_image) tvName.text = getString(R.string.select_image)
ivBg.setImageResource(R.drawable.ic_image) ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor) ivBg.setColorFilter(primaryTextColor)
root.setOnClickListener { selectImage() } root.setOnClickListener {
selectBgImage.launch("image/*")
}
} }
} }
requireContext().assets.list("bg")?.let { requireContext().assets.list("bg")?.let {
@ -159,25 +174,20 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
.show(requireActivity()) .show(requireActivity())
} }
binding.ivImport.setOnClickListener { binding.ivImport.setOnClickListener {
val importFormNet = "网络导入" selectImportDoc.launch(
val otherActions = arrayListOf(importFormNet) FilePickerParam(
FilePicker.selectFile( mode = FilePicker.FILE,
this@BgTextConfigDialog, title = getString(R.string.import_str),
requestCodeImport, allowExtensions = arrayOf("zip"),
title = getString(R.string.import_str), otherActions = arrayOf(importFormNet)
allowExtensions = arrayOf("zip"), )
otherActions = otherActions )
) { action ->
when (action) {
importFormNet -> importNetConfigAlert()
}
}
} }
binding.ivExport.setOnClickListener { binding.ivExport.setOnClickListener {
FilePicker.selectFolder( selectExportDir.launch(
this@BgTextConfigDialog, FilePickerParam(
requestCodeExport, title = getString(R.string.export_str)
title = getString(R.string.export_str) )
) )
} }
binding.ivDelete.setOnClickListener { binding.ivDelete.setOnClickListener {
@ -190,13 +200,6 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} }
} }
private fun selectImage() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, requestCodeBg)
}
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
private fun exportConfig(uri: Uri) { private fun exportConfig(uri: Uri) {
val exportFileName = if (ReadBookConfig.config.name.isBlank()) { val exportFileName = if (ReadBookConfig.config.name.isBlank()) {
@ -370,27 +373,6 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeBg -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri)
}
}
requestCodeImport -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
importConfig(uri)
}
}
requestCodeExport -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
exportConfig(uri)
}
}
}
}
private fun setBgFromUri(uri: Uri) { private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentScheme()) { if (uri.toString().isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri) val doc = DocumentFile.fromSingleUri(requireContext(), uri)

@ -18,6 +18,7 @@ import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogHttpTtsEditBinding import io.legado.app.databinding.DialogHttpTtsEditBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemHttpTtsBinding import io.legado.app.databinding.ItemHttpTtsBinding
@ -25,6 +26,8 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -33,10 +36,21 @@ import splitties.init.appCtx
class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private val binding by viewBinding(DialogRecyclerViewBinding::bind) private val binding by viewBinding(DialogRecyclerViewBinding::bind)
private val ttsUrlKey = "ttsUrlKey"
lateinit var adapter: Adapter lateinit var adapter: Adapter
private val viewModel: SpeakEngineViewModel by viewModels() private val viewModel: SpeakEngineViewModel by viewModels()
private var httpTTSData: LiveData<List<HttpTTS>>? = null private var httpTTSData: LiveData<List<HttpTTS>>? = null
var engineId = appCtx.getPrefLong(PreferKey.speakEngine) private var engineId = appCtx.getPrefLong(PreferKey.speakEngine)
private val importDocResult = registerForActivityResult(FilePicker()) {
it?.let {
viewModel.importLocal(it)
}
}
private val exportDirResult = registerForActivityResult(FilePicker()) {
it?.let {
viewModel.export(it)
}
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -100,10 +114,45 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> editHttpTTS() R.id.menu_add -> editHttpTTS()
R.id.menu_default -> viewModel.importDefault() R.id.menu_default -> viewModel.importDefault()
R.id.menu_import_local -> importDocResult.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_onLine -> importAlert()
R.id.menu_export -> exportDirResult.launch(null)
} }
return true return true
} }
private fun importAlert() {
val aCache = ACache.get(requireContext(), cacheDir = false)
val cacheUrls: MutableList<String> = aCache
.getAsString(ttsUrlKey)
?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf()
alert(R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setFilterValues(cacheUrls)
editView.delCallBack = {
cacheUrls.remove(it)
aCache.put(ttsUrlKey, cacheUrls.joinToString(","))
}
}
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let { url ->
if (!cacheUrls.contains(url)) {
cacheUrls.add(0, url)
aCache.put(ttsUrlKey, cacheUrls.joinToString(","))
}
viewModel.importOnLine(url)
}
}
}.show()
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun editHttpTTS(v: HttpTTS? = null) { private fun editHttpTTS(v: HttpTTS? = null) {
val httpTTS = v?.copy() ?: HttpTTS() val httpTTS = v?.copy() ?: HttpTTS()
@ -168,7 +217,6 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
} }
} }
} }
} }

@ -1,12 +1,12 @@
package io.legado.app.ui.book.read.config package io.legado.app.ui.book.read.config
import android.app.Application import android.app.Application
import android.net.Uri
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.utils.GSON import io.legado.app.utils.*
import io.legado.app.utils.fromJsonArray
import rxhttp.wrapper.param.RxHttp import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toText import rxhttp.wrapper.param.toText
@ -18,18 +18,52 @@ class SpeakEngineViewModel(application: Application) : BaseViewModel(application
} }
} }
fun importOnLine(url: String, finally: (msg: String) -> Unit) { fun importOnLine(url: String) {
execute { execute {
RxHttp.get(url).toText("utf-8").await().let { json -> RxHttp.get(url).toText("utf-8").await().let { json ->
GSON.fromJsonArray<HttpTTS>(json)?.let { import(json)
appDb.httpTTSDao.insert(*it.toTypedArray())
}
} }
}.onSuccess { }.onSuccess {
finally("导入成功") toastOnUi("导入成功")
}.onError { }.onError {
finally("导入失败") toastOnUi("导入失败")
} }
} }
fun importLocal(uri: Uri) {
execute {
uri.readText(context)?.let {
import(it)
}
}.onSuccess {
toastOnUi("导入成功")
}.onError {
toastOnUi("导入失败")
}
}
fun import(text: String) {
when {
text.isJsonArray() -> {
GSON.fromJsonArray<HttpTTS>(text)?.let {
appDb.httpTTSDao.insert(*it.toTypedArray())
}
}
text.isJsonObject() -> {
GSON.fromJsonObject<HttpTTS>(text)?.let {
appDb.httpTTSDao.insert(it)
}
}
else -> {
throw Exception("格式不对")
}
}
}
fun export(uri: Uri) {
execute {
val httpTTS = appDb.httpTTSDao.all
uri.writeBytes(context, "httpTts.json", GSON.toJson(httpTTS).toByteArray())
}
}
} }

@ -12,7 +12,6 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.legado.app.R import io.legado.app.R
@ -72,7 +71,6 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initView() = with(binding) { private fun initView() = with(binding) {
adapter = TocRegexAdapter(requireContext()) adapter = TocRegexAdapter(requireContext())
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.addItemDecoration(VerticalDivider(requireContext())) recyclerView.addItemDecoration(VerticalDivider(requireContext()))
recyclerView.adapter = adapter recyclerView.adapter = adapter
val itemTouchCallback = ItemTouchCallback(adapter) val itemTouchCallback = ItemTouchCallback(adapter)
@ -137,7 +135,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
if (!cacheUrls.contains(defaultUrl)) { if (!cacheUrls.contains(defaultUrl)) {
cacheUrls.add(0, defaultUrl) cacheUrls.add(0, defaultUrl)
} }
requireContext().alert(titleResource = R.string.import_book_source_on_line) { requireContext().alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater) val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
alertBinding.apply { alertBinding.apply {
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)

@ -1,6 +1,5 @@
package io.legado.app.ui.book.source.debug package io.legado.app.ui.book.source.debug
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -12,10 +11,8 @@ import io.legado.app.databinding.ActivitySourceDebugBinding
import io.legado.app.help.LocalConfig import io.legado.app.help.LocalConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.startActivityForResult
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,7 +23,11 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
private lateinit var adapter: BookSourceDebugAdapter private lateinit var adapter: BookSourceDebugAdapter
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private val qrRequestCode = 101 private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it?.let {
startSearch(it)
}
}
override fun getViewBinding(): ActivitySourceDebugBinding { override fun getViewBinding(): ActivitySourceDebugBinding {
return ActivitySourceDebugBinding.inflate(layoutInflater) return ActivitySourceDebugBinding.inflate(layoutInflater)
@ -96,7 +97,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_scan -> { R.id.menu_scan -> {
startActivityForResult<QrCodeActivity>(qrRequestCode) qrCodeResult.launch(null)
} }
R.id.menu_help -> showHelp() R.id.menu_help -> showHelp()
} }
@ -108,16 +109,4 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
TextDialog.show(supportFragmentManager, text, TextDialog.MD) TextDialog.show(supportFragmentManager, text, TextDialog.MD)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> {
if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
startSearch(it)
}
}
}
}
}
} }

@ -1,7 +1,6 @@
package io.legado.app.ui.book.source.edit package io.legado.app.ui.book.source.edit
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
@ -25,10 +24,10 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.login.SourceLogin import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
@ -36,13 +35,10 @@ import kotlin.math.abs
class BookSourceEditActivity : class BookSourceEditActivity :
VMBaseActivity<ActivityBookSourceEditBinding, BookSourceEditViewModel>(false), VMBaseActivity<ActivityBookSourceEditBinding, BookSourceEditViewModel>(false),
FilePickerDialog.CallBack,
KeyboardToolPop.CallBack { KeyboardToolPop.CallBack {
override val viewModel: BookSourceEditViewModel override val viewModel: BookSourceEditViewModel
by viewModels() by viewModels()
private val qrRequestCode = 101
private val selectPathRequestCode = 102
private val adapter = BookSourceEditAdapter() private val adapter = BookSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList() private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val searchEntities: ArrayList<EditEntity> = ArrayList() private val searchEntities: ArrayList<EditEntity> = ArrayList()
@ -50,6 +46,20 @@ class BookSourceEditActivity :
private val infoEntities: ArrayList<EditEntity> = ArrayList() private val infoEntities: ArrayList<EditEntity> = ArrayList()
private val tocEntities: ArrayList<EditEntity> = ArrayList() private val tocEntities: ArrayList<EditEntity> = ArrayList()
private val contentEntities: ArrayList<EditEntity> = ArrayList() private val contentEntities: ArrayList<EditEntity> = ArrayList()
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
viewModel.importSource(it) { source ->
upRecyclerView(source)
}
}
private val selectDoc = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
sendText(uri.toString())
} else {
sendText(uri.path.toString())
}
}
private var mSoftKeyboardTool: PopupWindow? = null private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false private var mIsSoftKeyBoardShowing = false
@ -98,7 +108,7 @@ class BookSourceEditActivity :
} }
R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource())) R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_qr_code_camera -> startActivityForResult<QrCodeActivity>(qrRequestCode) R.id.menu_qr_code_camera -> qrCodeResult.launch(null)
R.id.menu_share_str -> share(GSON.toJson(getSource())) R.id.menu_share_str -> share(GSON.toJson(getSource()))
R.id.menu_share_qr -> shareWithQr( R.id.menu_share_qr -> shareWithQr(
GSON.toJson(getSource()), GSON.toJson(getSource()),
@ -397,7 +407,11 @@ class BookSourceEditActivity :
0 -> insertText(AppConst.urlOption) 0 -> insertText(AppConst.urlOption)
1 -> showRuleHelp() 1 -> showRuleHelp()
2 -> showRegexHelp() 2 -> showRegexHelp()
3 -> FilePicker.selectFile(this, selectPathRequestCode) 3 -> selectDoc.launch(
FilePickerParam(
mode = FilePicker.FILE
)
)
} }
} }
} }
@ -425,28 +439,6 @@ class BookSourceEditActivity :
mSoftKeyboardTool?.dismiss() mSoftKeyboardTool?.dismiss()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
viewModel.importSource(it) { source ->
upRecyclerView(source)
}
}
}
selectPathRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
sendText(uri.toString())
} else {
sendText(uri.path.toString())
}
}
}
}
}
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
val rect = Rect() val rect = Rect()

@ -1,7 +1,6 @@
package io.legado.app.ui.book.source.manage package io.legado.app.ui.book.source.manage
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -31,9 +30,9 @@ import io.legado.app.service.help.CheckSource
import io.legado.app.ui.association.ImportBookSourceActivity import io.legado.app.ui.association.ImportBookSourceActivity
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
@ -45,15 +44,11 @@ import java.io.File
class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceViewModel>(), class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceViewModel>(),
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
BookSourceAdapter.CallBack, BookSourceAdapter.CallBack,
FilePickerDialog.CallBack,
SelectActionBar.CallBack, SelectActionBar.CallBack,
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
override val viewModel: BookSourceViewModel override val viewModel: BookSourceViewModel
by viewModels() by viewModels()
private val importRecordKey = "bookSourceRecordKey" private val importRecordKey = "bookSourceRecordKey"
private val qrRequestCode = 101
private val importRequestCode = 132
private val exportRequestCode = 65
private lateinit var adapter: BookSourceAdapter private lateinit var adapter: BookSourceAdapter
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private var bookSourceLiveDate: LiveData<List<BookSource>>? = null private var bookSourceLiveDate: LiveData<List<BookSource>>? = null
@ -62,6 +57,37 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private var sort = Sort.Default private var sort = Sort.Default
private var sortAscending = true private var sortAscending = true
private var snackBar: Snackbar? = null private var snackBar: Snackbar? = null
private val qrResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
startActivity<ImportBookSourceActivity> {
putExtra("source", it)
}
}
private val importDoc = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
try {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportBookSourceActivity> {
putExtra("dataKey", dataKey)
}
}
} catch (e: Exception) {
toastOnUi("readTextError:${e.localizedMessage}")
}
}
private val exportDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
override fun getViewBinding(): ActivityBookSourceBinding { override fun getViewBinding(): ActivityBookSourceBinding {
return ActivityBookSourceBinding.inflate(layoutInflater) return ActivityBookSourceBinding.inflate(layoutInflater)
@ -95,15 +121,19 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_add_book_source -> startActivity<BookSourceEditActivity>() R.id.menu_add_book_source -> startActivity<BookSourceEditActivity>()
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode) R.id.menu_import_qr -> qrResult.launch(null)
R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) { R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) {
startActivity(Intent.createChooser(it, getString(R.string.share_selected_source))) startActivity(Intent.createChooser(it, getString(R.string.share_selected_source)))
} }
R.id.menu_group_manage -> R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage") GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_import_source_local -> FilePicker R.id.menu_import_local -> importDoc.launch(
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) FilePickerParam(
R.id.menu_import_source_onLine -> showImportDialog() mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_sort_manual -> { R.id.menu_sort_manual -> {
item.isChecked = true item.isChecked = true
sortCheck(Sort.Default) sortCheck(Sort.Default)
@ -292,7 +322,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray()) R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray())
R.id.menu_add_group -> selectionAddToGroups() R.id.menu_add_group -> selectionAddToGroups()
R.id.menu_remove_group -> selectionRemoveFromGroups() R.id.menu_remove_group -> selectionRemoveFromGroups()
R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) R.id.menu_export_selection -> exportDir.launch(null)
} }
return true return true
} }
@ -372,7 +402,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
.getAsString(importRecordKey) .getAsString(importRecordKey)
?.splitNotBlank(",") ?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_book_source_on_line) { alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
@ -467,46 +497,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
startActivity<ImportBookSourceActivity> {
putExtra("source", it)
}
}
}
importRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
try {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportBookSourceActivity> {
putExtra("dataKey", dataKey)
}
}
} catch (e: Exception) {
toastOnUi("readTextError:${e.localizedMessage}")
}
}
}
exportRequestCode -> {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
}
}
}
override fun finish() { override fun finish() {
if (searchView.query.isNullOrEmpty()) { if (searchView.query.isNullOrEmpty()) {
super.finish() super.finish()

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseFragment import io.legado.app.base.VMBaseFragment
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.DialogBookmarkBinding import io.legado.app.databinding.DialogBookmarkBinding
import io.legado.app.databinding.FragmentBookmarkBinding import io.legado.app.databinding.FragmentBookmarkBinding
@ -35,7 +36,9 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.bookMarkCallBack = this viewModel.bookMarkCallBack = this
initRecyclerView() initRecyclerView()
initData() viewModel.bookData.observe(this) {
initData(it)
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -46,20 +49,20 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
} }
private fun initData() { private fun initData(book: Book) {
viewModel.book?.let { book -> bookmarkLiveData?.removeObservers(viewLifecycleOwner)
bookmarkLiveData?.removeObservers(viewLifecycleOwner) bookmarkLiveData =
bookmarkLiveData = LivePagedListBuilder(
LivePagedListBuilder( appDb.bookmarkDao.observeByBook(book.bookUrl, book.name, book.author), 20
appDb.bookmarkDao.observeByBook(book.bookUrl, book.name, book.author), 20 ).build()
).build() bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) })
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) })
}
} }
override fun startBookmarkSearch(newText: String?) { override fun startBookmarkSearch(newText: String?) {
if (newText.isNullOrBlank()) { if (newText.isNullOrBlank()) {
initData() viewModel.bookData.value?.let {
initData(it)
}
} else { } else {
bookmarkLiveData?.removeObservers(viewLifecycleOwner) bookmarkLiveData?.removeObservers(viewLifecycleOwner)
bookmarkLiveData = LivePagedListBuilder( bookmarkLiveData = LivePagedListBuilder(

@ -6,16 +6,15 @@ import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.fragment.app.FragmentPagerAdapter
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityChapterListBinding import io.legado.app.databinding.ActivityChapterListBinding
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.visible import io.legado.app.utils.visible
@ -35,11 +34,15 @@ class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterLi
tabLayout = binding.titleBar.findViewById(R.id.tab_layout) tabLayout = binding.titleBar.findViewById(R.id.tab_layout)
tabLayout.isTabIndicatorFullWidth = false tabLayout.isTabIndicatorFullWidth = false
tabLayout.setSelectedTabIndicatorColor(accentColor) tabLayout.setSelectedTabIndicatorColor(accentColor)
intent.getStringExtra("bookUrl")?.let { binding.viewPager.adapter = TabFragmentPageAdapter()
viewModel.initBook(it) { TabLayoutMediator(tabLayout, binding.viewPager) { tab, position ->
binding.viewPager.adapter = TabFragmentPageAdapter(supportFragmentManager) when (position) {
tabLayout.setupWithViewPager(binding.viewPager) 0 -> tab.setText(R.string.chapter_list)
else -> tab.setText(R.string.bookmark)
} }
}.attach()
intent.getStringExtra("bookUrl")?.let {
viewModel.initBook(it)
} }
} }
@ -72,23 +75,16 @@ class ChapterListActivity : VMBaseActivity<ActivityChapterListBinding, ChapterLi
return super.onCompatCreateOptionsMenu(menu) return super.onCompatCreateOptionsMenu(menu)
} }
private inner class TabFragmentPageAdapter(fm: FragmentManager) : private inner class TabFragmentPageAdapter : FragmentStateAdapter(this) {
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return when (position) {
1 -> BookmarkFragment()
else -> ChapterListFragment()
}
}
override fun getCount(): Int { override fun getItemCount(): Int {
return 2 return 2
} }
override fun getPageTitle(position: Int): CharSequence { override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
1 -> getString(R.string.bookmark) 0 -> ChapterListFragment()
else -> getString(R.string.chapter_list) else -> BookmarkFragment()
} }
} }

@ -49,7 +49,9 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
ivChapterBottom.setColorFilter(btc) ivChapterBottom.setColorFilter(btc)
initRecyclerView() initRecyclerView()
initView() initView()
initBook() viewModel.bookData.observe(this@ChapterListFragment) {
initBook(it)
}
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -73,15 +75,13 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initBook() { private fun initBook(book: Book) {
launch { launch {
initDoc() initDoc()
viewModel.book?.let { durChapterIndex = book.durChapterIndex
durChapterIndex = it.durChapterIndex binding.tvCurrentChapterInfo.text =
binding.tvCurrentChapterInfo.text = "${book.durChapterTitle}(${book.durChapterIndex + 1}/${book.totalChapterNum})"
"${it.durChapterTitle}(${it.durChapterIndex + 1}/${it.totalChapterNum})" initCacheFileNames(book)
initCacheFileNames(it)
}
} }
} }
@ -108,7 +108,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
override fun observeLiveBus() { override fun observeLiveBus() {
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter -> observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
viewModel.book?.bookUrl?.let { bookUrl -> viewModel.bookData.value?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) { if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(chapter.getFileName()) adapter.cacheFileNames.add(chapter.getFileName())
adapter.notifyItemChanged(chapter.index, true) adapter.notifyItemChanged(chapter.index, true)
@ -130,7 +130,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} }
override val isLocalBook: Boolean override val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true get() = viewModel.bookData.value?.isLocalBook() == true
override fun durChapterIndex(): Int { override fun durChapterIndex(): Int {
return min(durChapterIndex, adapter.itemCount) return min(durChapterIndex, adapter.itemCount)

@ -2,22 +2,23 @@ package io.legado.app.ui.book.toc
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
class ChapterListViewModel(application: Application) : BaseViewModel(application) { class ChapterListViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = "" var bookUrl: String = ""
var book: Book? = null var bookData = MutableLiveData<Book>()
var chapterCallBack: ChapterListCallBack? = null var chapterCallBack: ChapterListCallBack? = null
var bookMarkCallBack: BookmarkCallBack? = null var bookMarkCallBack: BookmarkCallBack? = null
fun initBook(bookUrl: String, success: () -> Unit) { fun initBook(bookUrl: String) {
this.bookUrl = bookUrl this.bookUrl = bookUrl
execute { execute {
book = appDb.bookDao.getBook(bookUrl) appDb.bookDao.getBook(bookUrl)?.let {
}.onSuccess { bookData.postValue(it)
success.invoke() }
} }
} }

@ -0,0 +1,26 @@
package io.legado.app.ui.book.toc
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class TocActivityResult : ActivityResultContract<String, Pair<Int, Int>?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChapterListActivity::class.java)
.putExtra("bookUrl", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Pair<Int, Int>? {
if (resultCode == RESULT_OK) {
intent?.let {
return Pair(
it.getIntExtra("index", 0),
it.getIntExtra("chapterPos", 0)
)
}
}
return null
}
}

@ -2,31 +2,105 @@ package io.legado.app.ui.config
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment import io.legado.app.base.BasePreferenceFragment
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.LocalConfig import io.legado.app.help.LocalConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.BookWebDav
import io.legado.app.help.storage.ImportOldData
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.applyTint import io.legado.app.utils.*
import io.legado.app.utils.getPrefString import kotlinx.coroutines.Dispatchers
import splitties.init.appCtx
class BackupConfigFragment : BasePreferenceFragment(), class BackupConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
FilePickerDialog.CallBack {
private val selectBackupPath = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
} else {
AppConfig.backupPath = uri.path
}
}
private val backupDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
Backup.backup(appCtx, uri.toString())
}.onSuccess {
appCtx.toastOnUi(R.string.backup_success)
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Backup.backup(appCtx, path)
}.onSuccess {
appCtx.toastOnUi(R.string.backup_success)
}
}
}
}
private val restoreDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
Restore.restore(appCtx, uri.toString())
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Restore.restore(appCtx, path)
}
}
}
}
private val restoreOld = registerForActivityResult(FilePicker()) { uri ->
uri?.let {
ImportOldData.importUri(appCtx, uri)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_backup) addPreferencesFromResource(R.xml.pref_config_backup)
@ -53,7 +127,7 @@ class BackupConfigFragment : BasePreferenceFragment(),
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legado.app.ui.widget.prefs.Preference>("web_dav_restore") findPreference<io.legado.app.ui.widget.prefs.Preference>("web_dav_restore")
?.onLongClick = { BackupRestoreUi.restoreByFolder(this) } ?.onLongClick = { restoreDir.launch(null) }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -135,11 +209,11 @@ class BackupConfigFragment : BasePreferenceFragment(),
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) { when (preference?.key) {
PreferKey.backupPath -> BackupRestoreUi.selectBackupFolder(this) PreferKey.backupPath -> selectBackupPath.launch(null)
PreferKey.restoreIgnore -> restoreIgnore() PreferKey.restoreIgnore -> restoreIgnore()
"web_dav_backup" -> BackupRestoreUi.backup(this) "web_dav_backup" -> backup()
"web_dav_restore" -> BackupRestoreUi.restore(this) "web_dav_restore" -> restore()
"import_old" -> BackupRestoreUi.importOldData(this) "import_old" -> restoreOld.launch(null)
} }
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
@ -159,8 +233,81 @@ class BackupConfigFragment : BasePreferenceFragment(),
}.show() }.show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) fun backup() {
BackupRestoreUi.onActivityResult(requestCode, resultCode, data) val backupPath = AppConfig.backupPath
if (backupPath.isNullOrEmpty()) {
backupDir.launch(null)
} else {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc?.canWrite() == true) {
Coroutine.async {
Backup.backup(requireContext(), backupPath)
}.onSuccess {
toastOnUi(R.string.backup_success)
}
} else {
backupDir.launch(null)
}
} else {
backupUsePermission(backupPath)
}
}
} }
private fun backupUsePermission(path: String) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async {
AppConfig.backupPath = path
Backup.backup(requireContext(), path)
}.onSuccess {
toastOnUi(R.string.backup_success)
}
}
.request()
}
fun restore() {
Coroutine.async(context = Dispatchers.Main) {
BookWebDav.showRestoreDialog(requireContext())
}.onError {
longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。")
val backupPath = getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc?.canWrite() == true) {
Restore.restore(requireContext(), backupPath)
} else {
restoreDir.launch(null)
}
} else {
restoreUsePermission(backupPath)
}
} else {
restoreDir.launch(null)
}
}
}
private fun restoreUsePermission(path: String) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async {
AppConfig.backupPath = path
Restore.restoreDatabase(path)
Restore.restoreConfig(path)
}
}
.request()
}
} }

@ -1,195 +0,0 @@
package io.legado.app.ui.config
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.BookWebDav
import io.legado.app.help.storage.ImportOldData
import io.legado.app.help.storage.Restore
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.utils.getPrefString
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.longToast
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.Main
import splitties.init.appCtx
object BackupRestoreUi {
private const val selectFolderRequestCode = 21
private const val backupSelectRequestCode = 22
private const val restoreSelectRequestCode = 33
private const val oldDataRequestCode = 11
fun backup(fragment: Fragment) {
val backupPath = AppConfig.backupPath
if (backupPath.isNullOrEmpty()) {
selectBackupFolder(fragment, backupSelectRequestCode)
} else {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri)
if (doc?.canWrite() == true) {
Coroutine.async {
Backup.backup(fragment.requireContext(), backupPath)
}.onSuccess {
fragment.toastOnUi(R.string.backup_success)
}
} else {
selectBackupFolder(fragment, backupSelectRequestCode)
}
} else {
backupUsePermission(fragment, backupPath)
}
}
}
private fun backupUsePermission(
fragment: Fragment,
path: String
) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async {
AppConfig.backupPath = path
Backup.backup(fragment.requireContext(), path)
}.onSuccess {
fragment.toastOnUi(R.string.backup_success)
}
}
.request()
}
fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) {
FilePicker.selectFolder(fragment, requestCode)
}
fun restore(fragment: Fragment) {
Coroutine.async(context = Main) {
BookWebDav.showRestoreDialog(fragment.requireContext())
}.onError {
fragment.longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。")
val backupPath = fragment.getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri)
if (doc?.canWrite() == true) {
Restore.restore(fragment.requireContext(), backupPath)
} else {
selectBackupFolder(fragment, restoreSelectRequestCode)
}
} else {
restoreUsePermission(fragment, backupPath)
}
} else {
selectBackupFolder(fragment, restoreSelectRequestCode)
}
}
}
fun restoreByFolder(fragment: Fragment) {
selectBackupFolder(fragment, restoreSelectRequestCode)
}
private fun restoreUsePermission(fragment: Fragment, path: String) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
Coroutine.async {
AppConfig.backupPath = path
Restore.restoreDatabase(path)
Restore.restoreConfig(path)
}
}
.request()
}
fun importOldData(fragment: Fragment) {
FilePicker.selectFolder(fragment, oldDataRequestCode)
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
backupSelectRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
Backup.backup(appCtx, uri.toString())
}.onSuccess {
appCtx.toastOnUi(R.string.backup_success)
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Backup.backup(appCtx, path)
}.onSuccess {
appCtx.toastOnUi(R.string.backup_success)
}
}
}
}
}
restoreSelectRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
Coroutine.async {
Restore.restore(appCtx, uri.toString())
}
} else {
uri.path?.let { path ->
AppConfig.backupPath = path
Coroutine.async {
Restore.restore(appCtx, path)
}
}
}
}
}
selectFolderRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
appCtx.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
} else {
AppConfig.backupPath = uri.path
}
}
}
oldDataRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
ImportOldData.importUri(appCtx, uri)
}
}
}
}
}

@ -1,7 +1,6 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -10,6 +9,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -38,14 +38,16 @@ import java.io.File
class OtherConfigFragment : BasePreferenceFragment(), class OtherConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeCover = 231
private val packageManager = appCtx.packageManager private val packageManager = appCtx.packageManager
private val componentName = ComponentName( private val componentName = ComponentName(
appCtx, appCtx,
SharedReceiverActivity::class.java.name SharedReceiverActivity::class.java.name
) )
private val webPort get() = getPrefInt(PreferKey.webPort, 1122) private val webPort get() = getPrefInt(PreferKey.webPort, 1122)
private val selectCoverImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
it ?: return@registerForActivityResult
setCoverFromUri(it)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
@ -88,13 +90,13 @@ class OtherConfigFragment : BasePreferenceFragment(),
} }
PreferKey.cleanCache -> clearCache() PreferKey.cleanCache -> clearCache()
PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectImage(requestCodeCover) selectCoverImage.launch("image/*")
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.defaultCover) removePref(PreferKey.defaultCover)
} else { } else {
selectImage(requestCodeCover) selectCoverImage.launch("image/*")
} }
} }
} }
@ -183,14 +185,6 @@ class OtherConfigFragment : BasePreferenceFragment(),
}.show() }.show()
} }
@Suppress("SameParameterValue")
private fun selectImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
private fun isProcessTextEnabled(): Boolean { private fun isProcessTextEnabled(): Boolean {
return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
} }
@ -246,15 +240,4 @@ class OtherConfigFragment : BasePreferenceFragment(),
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeCover -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
setCoverFromUri(uri)
}
}
}
}
} }

@ -1,8 +1,6 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -11,6 +9,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
@ -38,8 +37,18 @@ import java.io.File
class ThemeConfigFragment : BasePreferenceFragment(), class ThemeConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeBgImage = 234 private val selectLightBg = registerForActivityResult(ActivityResultContracts.GetContent()) {
private val requestCodeBgImageN = 342 it ?: return@registerForActivityResult
setBgFromUri(it, PreferKey.bgImage) {
upTheme(false)
}
}
private val selectDarkBg = registerForActivityResult(ActivityResultContracts.GetContent()) {
it ?: return@registerForActivityResult
setBgFromUri(it, PreferKey.bgImageN) {
upTheme(true)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme) addPreferencesFromResource(R.xml.pref_config_theme)
@ -186,26 +195,26 @@ class ThemeConfigFragment : BasePreferenceFragment(),
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key)
PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) {
selectImage(requestCodeBgImage) selectLightBg.launch("image/*")
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.bgImage) removePref(PreferKey.bgImage)
upTheme(false) upTheme(false)
} else { } else {
selectImage(requestCodeBgImage) selectLightBg.launch("image/*")
} }
} }
} }
PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) { PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) {
selectImage(requestCodeBgImageN) selectDarkBg.launch("image/*")
} else { } else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) { if (i == 0) {
removePref(PreferKey.bgImageN) removePref(PreferKey.bgImageN)
upTheme(true) upTheme(true)
} else { } else {
selectImage(requestCodeBgImageN) selectDarkBg.launch("image/*")
} }
} }
} }
@ -213,13 +222,6 @@ class ThemeConfigFragment : BasePreferenceFragment(),
return super.onPreferenceTreeClick(preference) return super.onPreferenceTreeClick(preference)
} }
private fun selectImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun saveThemeAlert(key: String) { private fun saveThemeAlert(key: String) {
alert(R.string.theme_name) { alert(R.string.theme_name) {
@ -302,24 +304,4 @@ class ThemeConfigFragment : BasePreferenceFragment(),
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeBgImage -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri, PreferKey.bgImage) {
upTheme(false)
}
}
}
requestCodeBgImageN -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
setBgFromUri(uri, PreferKey.bgImageN) {
upTheme(true)
}
}
}
}
}
} }

@ -0,0 +1,43 @@
package io.legado.app.ui.document
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
@Suppress("unused")
class FilePicker : ActivityResultContract<FilePickerParam, Uri?>() {
companion object {
const val DIRECTORY = 0
const val FILE = 1
}
override fun createIntent(context: Context, input: FilePickerParam?): Intent {
val intent = Intent(context, FilePickerActivity::class.java)
input?.let {
intent.putExtra("mode", it.mode)
intent.putExtra("title", it.title)
intent.putExtra("allowExtensions", it.allowExtensions)
intent.putExtra("otherActions", it.otherActions)
}
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
if (resultCode == RESULT_OK) {
return intent?.data
}
return null
}
}
@Suppress("ArrayInDataClass")
data class FilePickerParam(
var mode: Int = 0,
var title: String? = null,
var allowExtensions: Array<String> = arrayOf(),
var otherActions: Array<String>? = null,
)

@ -0,0 +1,137 @@
package io.legado.app.ui.document
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.constant.Theme
import io.legado.app.databinding.ActivityTranslucenceBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.utils.isContentScheme
import java.io.File
class FilePickerActivity :
BaseActivity<ActivityTranslucenceBinding>(
theme = Theme.Transparent
), FilePickerDialog.CallBack {
private val selectDocTree =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
onResult(Intent().setData(it))
}
private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
onResult(Intent().setData(it))
}
override fun getViewBinding(): ActivityTranslucenceBinding {
return ActivityTranslucenceBinding.inflate(layoutInflater)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
val mode = intent.getIntExtra("mode", 0)
val allowExtensions = intent.getStringArrayExtra("allowExtensions")
val selectList = if (mode == FilePicker.DIRECTORY) {
arrayListOf(getString(R.string.sys_folder_picker))
} else {
arrayListOf(getString(R.string.sys_file_picker))
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
selectList.add(getString(R.string.app_folder_picker))
}
intent.getStringArrayListExtra("otherActions")?.let {
selectList.addAll(it)
}
val title = intent.getStringExtra("title") ?: let {
if (mode == FilePicker.DIRECTORY) {
return@let getString(R.string.select_folder)
} else {
return@let getString(R.string.select_file)
}
}
alert(title) {
items(selectList) { _, index ->
when (index) {
0 -> if (mode == FilePicker.DIRECTORY) {
selectDocTree.launch(null)
} else {
selectDoc.launch(typesOfExtensions(allowExtensions))
}
1 -> if (mode == FilePicker.DIRECTORY) {
checkPermissions {
FilePickerDialog.show(
supportFragmentManager,
mode = FilePicker.DIRECTORY
)
}
} else {
checkPermissions {
FilePickerDialog.show(
supportFragmentManager,
mode = FilePicker.FILE,
allowExtensions = allowExtensions
)
}
}
else -> {
val path = selectList[index]
val uri = if (path.isContentScheme()) {
Uri.fromFile(File(path))
} else {
Uri.parse(path)
}
onResult(Intent().setData(uri))
}
}
}
onCancelled {
finish()
}
}.show()
}
private fun checkPermissions(success: (() -> Unit)? = null) {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke()
}
.request()
}
private fun typesOfExtensions(allowExtensions: Array<String>?): Array<String> {
val types = hashSetOf<String>()
if (allowExtensions.isNullOrEmpty()) {
types.add("*/*")
} else {
allowExtensions.forEach {
when (it) {
"*" -> types.add("*/*")
"txt", "xml" -> types.add("text/*")
else -> {
val mime = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("json")
?: "application/octet-stream"
types.add(mime)
}
}
}
}
return types.toTypedArray()
}
override fun onResult(data: Intent) {
if (data.data != null) {
setResult(RESULT_OK, data)
}
finish()
}
}

@ -1,6 +1,6 @@
package io.legado.app.ui.filepicker package io.legado.app.ui.document
import android.app.Activity import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -8,7 +8,6 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -16,10 +15,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.databinding.DialogFileChooserBinding import io.legado.app.databinding.DialogFileChooserBinding
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.adapter.FileAdapter import io.legado.app.ui.document.FilePicker.Companion.DIRECTORY
import io.legado.app.ui.filepicker.adapter.PathAdapter import io.legado.app.ui.document.FilePicker.Companion.FILE
import io.legado.app.ui.document.adapter.FileAdapter
import io.legado.app.ui.document.adapter.PathAdapter
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -33,12 +33,9 @@ class FilePickerDialog : DialogFragment(),
companion object { companion object {
const val tag = "FileChooserDialog" const val tag = "FileChooserDialog"
const val DIRECTORY = 0
const val FILE = 1
fun show( fun show(
manager: FragmentManager, manager: FragmentManager,
requestCode: Int,
mode: Int = FILE, mode: Int = FILE,
title: String? = null, title: String? = null,
initPath: String? = null, initPath: String? = null,
@ -51,7 +48,6 @@ class FilePickerDialog : DialogFragment(),
FilePickerDialog().apply { FilePickerDialog().apply {
val bundle = Bundle() val bundle = Bundle()
bundle.putInt("mode", mode) bundle.putInt("mode", mode)
bundle.putInt("requestCode", requestCode)
bundle.putString("title", title) bundle.putString("title", title)
bundle.putBoolean("isShowHomeDir", isShowHomeDir) bundle.putBoolean("isShowHomeDir", isShowHomeDir)
bundle.putBoolean("isShowUpDir", isShowUpDir) bundle.putBoolean("isShowUpDir", isShowUpDir)
@ -71,20 +67,6 @@ class FilePickerDialog : DialogFragment(),
override var isShowHomeDir: Boolean = false override var isShowHomeDir: Boolean = false
override var isShowUpDir: Boolean = true override var isShowUpDir: Boolean = true
override var isShowHideDir: Boolean = false override var isShowHideDir: Boolean = false
private val queryPermission =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
var hasPermission = true
it.forEach { (t, u) ->
if (!u) {
hasPermission = false
toastOnUi(t)
}
}
if (hasPermission) {
refreshCurrentDirPath(initPath)
}
}
private var requestCode: Int = 0
var title: String? = null var title: String? = null
private var initPath = FileUtils.getSdCardPath() private var initPath = FileUtils.getSdCardPath()
private var mode: Int = FILE private var mode: Int = FILE
@ -111,7 +93,6 @@ class FilePickerDialog : DialogFragment(),
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
view.setBackgroundResource(R.color.background_card) view.setBackgroundResource(R.color.background_card)
arguments?.let { arguments?.let {
requestCode = it.getInt("requestCode")
mode = it.getInt("mode", FILE) mode = it.getInt("mode", FILE)
title = it.getString("title") title = it.getString("title")
isShowHomeDir = it.getBoolean("isShowHomeDir") isShowHomeDir = it.getBoolean("isShowHomeDir")
@ -132,7 +113,7 @@ class FilePickerDialog : DialogFragment(),
} }
initMenu() initMenu()
initContentView() initContentView()
queryPermission.launch(Permissions.Group.STORAGE) refreshCurrentDirPath(initPath)
} }
private fun initMenu() { private fun initMenu() {
@ -168,11 +149,6 @@ class FilePickerDialog : DialogFragment(),
setData(it) setData(it)
dismissAllowingStateLoss() dismissAllowingStateLoss()
} }
else -> item?.title?.let {
(parentFragment as? CallBack)?.onMenuClick(it.toString())
(activity as? CallBack)?.onMenuClick(it.toString())
dismissAllowingStateLoss()
}
} }
return true return true
} }
@ -225,14 +201,16 @@ class FilePickerDialog : DialogFragment(),
private fun setData(path: String) { private fun setData(path: String) {
val data = Intent().setData(Uri.fromFile(File(path))) val data = Intent().setData(Uri.fromFile(File(path)))
(parentFragment as? CallBack) (parentFragment as? CallBack)?.onResult(data)
?.onActivityResult(requestCode, Activity.RESULT_OK, data) (activity as? CallBack)?.onResult(data)
(activity as? CallBack) }
?.onActivityResult(requestCode, Activity.RESULT_OK, data)
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
activity?.finish()
} }
interface CallBack { interface CallBack {
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) fun onResult(data: Intent)
fun onMenuClick(menu: String) {}
} }
} }

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.adapter package io.legado.app.ui.document.adapter
import android.content.Context import android.content.Context
@ -9,9 +9,9 @@ import io.legado.app.databinding.ItemFileFilepickerBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.getPrimaryDisabledTextColor import io.legado.app.lib.theme.getPrimaryDisabledTextColor
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.ui.filepicker.entity.FileItem import io.legado.app.ui.document.entity.FileItem
import io.legado.app.ui.filepicker.utils.ConvertUtils import io.legado.app.ui.document.utils.ConvertUtils
import io.legado.app.ui.filepicker.utils.FilePickerIcon import io.legado.app.ui.document.utils.FilePickerIcon
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import java.io.File import java.io.File

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.adapter package io.legado.app.ui.document.adapter
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
@ -6,8 +6,8 @@ import android.view.ViewGroup
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemPathFilepickerBinding import io.legado.app.databinding.ItemPathFilepickerBinding
import io.legado.app.ui.filepicker.utils.ConvertUtils import io.legado.app.ui.document.utils.ConvertUtils
import io.legado.app.ui.filepicker.utils.FilePickerIcon import io.legado.app.ui.document.utils.FilePickerIcon
import java.util.* import java.util.*

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.entity package io.legado.app.ui.document.entity
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.entity package io.legado.app.ui.document.entity
import java.io.Serializable import java.io.Serializable
import java.lang.reflect.Field import java.lang.reflect.Field

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.utils package io.legado.app.ui.document.utils
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.utils; package io.legado.app.ui.document.utils;
/** /**
* Generated by https://github.com/gzu-liyujiang/Image2ByteVar * Generated by https://github.com/gzu-liyujiang/Image2ByteVar

@ -1,284 +0,0 @@
package io.legado.app.ui.filepicker
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import io.legado.app.R
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
@Suppress("unused")
object FilePicker {
fun selectFolder(
activity: AppCompatActivity,
requestCode: Int,
title: String = activity.getString(R.string.select_folder),
otherActions: List<String>? = null,
otherFun: ((action: String) -> Unit)? = null
) {
val selectList = arrayListOf(activity.getString(R.string.sys_folder_picker))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
selectList.add(activity.getString(R.string.app_folder_picker))
}
otherActions?.let {
selectList.addAll(otherActions)
}
activity.alert(title = title) {
items(selectList) { _, index ->
when (index) {
0 -> {
kotlin.runCatching {
val intent = createSelectDirIntent()
activity.startActivityForResult(intent, requestCode)
}.onFailure {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
requestCode,
mode = FilePickerDialog.DIRECTORY
)
}
}
}
else -> {
val selectText = selectList[index]
if (selectText == activity.getString(R.string.app_folder_picker)) {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
requestCode,
mode = FilePickerDialog.DIRECTORY
)
}
} else {
otherFun?.invoke(selectText)
}
}
}
}
}.show()
}
fun selectFolder(
fragment: Fragment,
requestCode: Int,
title: String = fragment.getString(R.string.select_folder),
otherActions: List<String>? = null,
otherFun: ((action: String) -> Unit)? = null
) {
val selectList = arrayListOf(fragment.getString(R.string.sys_folder_picker))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
selectList.add(fragment.getString(R.string.app_folder_picker))
}
otherActions?.let {
selectList.addAll(otherActions)
}
fragment.alert(title = title) {
items(selectList) { _, index ->
when (index) {
0 -> {
kotlin.runCatching {
val intent = createSelectDirIntent()
fragment.startActivityForResult(intent, requestCode)
}.onFailure {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,
requestCode,
mode = FilePickerDialog.DIRECTORY
)
}
}
}
else -> {
val selectText = selectList[index]
if (selectText == fragment.getString(R.string.app_folder_picker)) {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,
requestCode,
mode = FilePickerDialog.DIRECTORY
)
}
} else {
otherFun?.invoke(selectText)
}
}
}
}
}.show()
}
fun selectFile(
activity: AppCompatActivity,
requestCode: Int,
title: String = activity.getString(R.string.select_file),
allowExtensions: Array<String> = arrayOf(),
otherActions: List<String>? = null,
otherFun: ((action: String) -> Unit)? = null
) {
val selectList = arrayListOf(activity.getString(R.string.sys_file_picker))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
selectList.add(activity.getString(R.string.app_file_picker))
}
otherActions?.let {
selectList.addAll(otherActions)
}
activity.alert(title = title) {
items(selectList) { _, index ->
when (index) {
0 -> {
kotlin.runCatching {
val intent = createSelectFileIntent()
intent.putExtra(
Intent.EXTRA_MIME_TYPES,
typesOfExtensions(allowExtensions)
)
activity.startActivityForResult(intent, requestCode)
}.onFailure {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
requestCode,
mode = FilePickerDialog.FILE,
allowExtensions = allowExtensions
)
}
}
}
else -> {
val selectText = selectList[index]
if (selectText == activity.getString(R.string.app_file_picker)) {
checkPermissions(activity) {
FilePickerDialog.show(
activity.supportFragmentManager,
requestCode,
mode = FilePickerDialog.FILE,
allowExtensions = allowExtensions
)
}
} else {
otherFun?.invoke(selectText)
}
}
}
}
}.show()
}
fun selectFile(
fragment: Fragment,
requestCode: Int,
title: String = fragment.getString(R.string.select_file),
allowExtensions: Array<String> = arrayOf(),
otherActions: List<String>? = null,
otherFun: ((action: String) -> Unit)? = null
) {
val selectList = arrayListOf(fragment.getString(R.string.sys_file_picker))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
selectList.add(fragment.getString(R.string.app_file_picker))
}
otherActions?.let {
selectList.addAll(otherActions)
}
fragment.alert(title = title) {
items(selectList) { _, index ->
when (index) {
0 -> {
kotlin.runCatching {
val intent = createSelectFileIntent()
intent.putExtra(
Intent.EXTRA_MIME_TYPES,
typesOfExtensions(allowExtensions)
)
fragment.startActivityForResult(intent, requestCode)
}.onFailure {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,
requestCode,
mode = FilePickerDialog.FILE,
allowExtensions = allowExtensions
)
}
}
}
else -> {
val selectText = selectList[index]
if (selectText == fragment.getString(R.string.app_file_picker)) {
checkPermissions(fragment) {
FilePickerDialog.show(
fragment.childFragmentManager,
requestCode,
mode = FilePickerDialog.FILE,
allowExtensions = allowExtensions
)
}
} else {
otherFun?.invoke(selectText)
}
}
}
}
}.show()
}
private fun createSelectFileIntent(): Intent {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
intent.type = "*/*"
return intent
}
private fun createSelectDirIntent(): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
return intent
}
private fun checkPermissions(fragment: Fragment, success: (() -> Unit)? = null) {
PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke()
}
.request()
}
private fun checkPermissions(activity: AppCompatActivity, success: (() -> Unit)? = null) {
PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
success?.invoke()
}
.request()
}
private fun typesOfExtensions(allowExtensions: Array<String>): Array<String> {
val types = hashSetOf<String>()
if (allowExtensions.isNullOrEmpty()) {
types.add("*/*")
} else {
allowExtensions.forEach {
when (it) {
"*" -> types.add("*/*")
"txt", "xml" -> types.add("text/*")
else -> types.add("application/$it")
}
}
}
return types.toTypedArray()
}
}

@ -1,8 +1,6 @@
package io.legado.app.ui.main.bookshelf package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -29,8 +27,8 @@ import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.group.GroupManageDialog import io.legado.app.ui.book.group.GroupManageDialog
import io.legado.app.ui.book.local.ImportBookActivity import io.legado.app.ui.book.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.main.MainViewModel import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.utils.* import io.legado.app.utils.*
@ -41,10 +39,8 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
*/ */
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf), class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener, TabLayout.OnTabSelectedListener,
FilePickerDialog.CallBack,
SearchView.OnQueryTextListener { SearchView.OnQueryTextListener {
private val requestCodeImportBookshelf = 312
private val binding by viewBinding(FragmentBookshelfBinding::bind) private val binding by viewBinding(FragmentBookshelfBinding::bind)
override val viewModel: BookshelfViewModel by viewModels() override val viewModel: BookshelfViewModel by viewModels()
private val activityViewModel: MainViewModel by activityViewModels() private val activityViewModel: MainViewModel by activityViewModels()
@ -53,6 +49,11 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
private var bookGroupLiveData: LiveData<List<BookGroup>>? = null private var bookGroupLiveData: LiveData<List<BookGroup>>? = null
private val bookGroups = mutableListOf<BookGroup>() private val bookGroups = mutableListOf<BookGroup>()
private val fragmentMap = hashMapOf<Long, BooksFragment>() private val fragmentMap = hashMapOf<Long, BooksFragment>()
private val importBookshelf = registerForActivityResult(FilePicker()) {
it?.readText(requireContext())?.let { text ->
viewModel.importBookshelf(text, selectedGroup.groupId)
}
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tabLayout = binding.titleBar.findViewById(R.id.tab_layout) tabLayout = binding.titleBar.findViewById(R.id.tab_layout)
@ -229,28 +230,16 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
noButton() noButton()
neutralButton(R.string.select_file) { neutralButton(R.string.select_file) {
FilePicker.selectFile( importBookshelf.launch(
this@BookshelfFragment, FilePickerParam(
requestCodeImportBookshelf, mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json") allowExtensions = arrayOf("txt", "json")
)
) )
} }
}.show() }.show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeImportBookshelf -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
uri.readText(requireContext())?.let {
viewModel.importBookshelf(it, selectedGroup.groupId)
}
}
}
}
}
private inner class TabFragmentPageAdapter(fm: FragmentManager) : private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

@ -73,6 +73,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val bookMap = hashMapOf<String, String?>() val bookMap = hashMapOf<String, String?>()
bookMap["name"] = it.name bookMap["name"] = it.name
bookMap["author"] = it.author bookMap["author"] = it.author
bookMap["intro"] = it.getDisplayIntro()
exportList.add(bookMap) exportList.add(bookMap)
} }
GSON.toJson(exportList) GSON.toJson(exportList)

@ -1,6 +1,5 @@
package io.legado.app.ui.main.my package io.legado.app.ui.main.my
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -21,10 +20,8 @@ import io.legado.app.ui.about.AboutActivity
import io.legado.app.ui.about.DonateActivity import io.legado.app.ui.about.DonateActivity
import io.legado.app.ui.about.ReadRecordActivity import io.legado.app.ui.about.ReadRecordActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.config.BackupRestoreUi
import io.legado.app.ui.config.ConfigActivity import io.legado.app.ui.config.ConfigActivity
import io.legado.app.ui.config.ConfigViewModel import io.legado.app.ui.config.ConfigViewModel
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.prefs.NameListPreference import io.legado.app.ui.widget.prefs.NameListPreference
@ -33,7 +30,7 @@ import io.legado.app.ui.widget.prefs.SwitchPreference
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
class MyFragment : BaseFragment(R.layout.fragment_my_config), FilePickerDialog.CallBack { class MyFragment : BaseFragment(R.layout.fragment_my_config) {
private val binding by viewBinding(FragmentMyConfigBinding::bind) private val binding by viewBinding(FragmentMyConfigBinding::bind)
@ -59,11 +56,6 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FilePickerDialog.C
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
BackupRestoreUi.onActivityResult(requestCode, resultCode, data)
}
/** /**
* 配置 * 配置
*/ */

@ -0,0 +1,23 @@
package io.legado.app.ui.qrcode
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class QrCodeResult : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, QrCodeActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
if (resultCode == RESULT_OK) {
intent?.getStringExtra("result")?.let {
return it
}
}
return null
}
}

@ -2,7 +2,6 @@ package io.legado.app.ui.replace
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -29,9 +28,9 @@ import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.ui.association.ImportReplaceRuleActivity import io.legado.app.ui.association.ImportReplaceRuleActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.replace.edit.ReplaceEditActivity import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -47,24 +46,52 @@ import java.io.File
class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRuleViewModel>(), class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRuleViewModel>(),
SearchView.OnQueryTextListener, SearchView.OnQueryTextListener,
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
FilePickerDialog.CallBack,
SelectActionBar.CallBack, SelectActionBar.CallBack,
ReplaceRuleAdapter.CallBack { ReplaceRuleAdapter.CallBack {
override val viewModel: ReplaceRuleViewModel by viewModels() override val viewModel: ReplaceRuleViewModel by viewModels()
private val importRecordKey = "replaceRuleRecordKey" private val importRecordKey = "replaceRuleRecordKey"
private val importRequestCode = 132
private val importRequestCodeQr = 133
private val exportRequestCode = 234
private val editActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setResult(RESULT_OK)
}
private lateinit var adapter: ReplaceRuleAdapter private lateinit var adapter: ReplaceRuleAdapter
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private var groups = hashSetOf<String>() private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null
private var dataInit = false private var dataInit = false
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
startActivity<ImportReplaceRuleActivity> {
putExtra("source", it)
}
}
private val editActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
setResult(RESULT_OK)
}
}
private val importDoc = registerForActivityResult(FilePicker()) { uri ->
kotlin.runCatching {
uri?.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportReplaceRuleActivity> {
putExtra("dataKey", dataKey)
}
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
private val exportDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
override fun getViewBinding(): ActivityReplaceRuleBinding { override fun getViewBinding(): ActivityReplaceRuleBinding {
return ActivityReplaceRuleBinding.inflate(layoutInflater) return ActivityReplaceRuleBinding.inflate(layoutInflater)
@ -189,10 +216,14 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
GroupManageDialog().show(supportFragmentManager, "groupManage") GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_source_local -> FilePicker R.id.menu_import_local -> importDoc.launch(
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) FilePickerParam(
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(importRequestCodeQr) mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_qr -> qrCodeResult.launch(null)
R.id.menu_help -> showHelp() R.id.menu_help -> showHelp()
else -> if (item.groupId == R.id.replace_group) { else -> if (item.groupId == R.id.replace_group) {
searchView.setQuery("group:${item.title}", true) searchView.setQuery("group:${item.title}", true)
@ -205,7 +236,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
when (item?.itemId) { when (item?.itemId) {
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection())
R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) R.id.menu_export_selection -> exportDir.launch(null)
} }
return false return false
} }
@ -263,47 +294,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
return false return false
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != RESULT_OK) return
when (requestCode) {
importRequestCode -> {
data?.data?.let { uri ->
kotlin.runCatching {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportReplaceRuleActivity> {
putExtra("dataKey", dataKey)
}
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
}
importRequestCodeQr -> {
data?.getStringExtra("result")?.let {
startActivity<ImportReplaceRuleActivity> {
putExtra("source", it)
}
}
}
exportRequestCode -> {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Coroutine.async { ReadBook.contentProcessor?.upReplaceRules() } Coroutine.async { ReadBook.contentProcessor?.upReplaceRules() }

@ -1,38 +1,47 @@
package io.legado.app.ui.rss.article package io.legado.app.ui.rss.article
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.fragment.app.FragmentStatePagerAdapter import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityRssArtivlesBinding import io.legado.app.databinding.ActivityRssArtivlesBinding
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.utils.gone import io.legado.app.utils.gone
import io.legado.app.utils.startActivityForResult
import io.legado.app.utils.visible import io.legado.app.utils.visible
class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewModel>() { class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewModel>() {
override val viewModel: RssSortViewModel override val viewModel: RssSortViewModel
by viewModels() by viewModels()
private val editSource = 12319
private val fragments = linkedMapOf<String, RssArticlesFragment>() private val fragments = linkedMapOf<String, RssArticlesFragment>()
private lateinit var adapter: TabFragmentPageAdapter private lateinit var adapter: TabFragmentPageAdapter
private val upSourceResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK) {
viewModel.initData(intent) {
upFragments()
}
}
}
override fun getViewBinding(): ActivityRssArtivlesBinding { override fun getViewBinding(): ActivityRssArtivlesBinding {
return ActivityRssArtivlesBinding.inflate(layoutInflater) return ActivityRssArtivlesBinding.inflate(layoutInflater)
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
adapter = TabFragmentPageAdapter(supportFragmentManager) adapter = TabFragmentPageAdapter()
binding.tabLayout.setupWithViewPager(binding.viewPager)
binding.viewPager.adapter = adapter binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = fragments.keys.elementAt(position)
}.attach()
viewModel.titleLiveData.observe(this, { viewModel.titleLiveData.observe(this, {
binding.titleBar.title = it binding.titleBar.title = it
}) })
@ -49,9 +58,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let { R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let {
startActivityForResult<RssSourceEditActivity>(editSource) { upSourceResult.launch(
putExtra("data", it) Intent(this, RssSourceEditActivity::class.java)
} .putExtra("data", it)
)
} }
R.id.menu_clear -> { R.id.menu_clear -> {
viewModel.url?.let { viewModel.url?.let {
@ -79,35 +89,15 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { private inner class TabFragmentPageAdapter : FragmentStateAdapter(this) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
editSource -> if (resultCode == Activity.RESULT_OK) {
viewModel.initData(intent) {
upFragments()
}
}
}
}
private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getPageTitle(position: Int): CharSequence { override fun getItemCount(): Int {
return fragments.keys.elementAt(position) return fragments.size
} }
override fun getItem(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return fragments.values.elementAt(position) return fragments.values.elementAt(position)
} }
override fun getCount(): Int {
return fragments.size
}
} }
} }

@ -23,8 +23,8 @@ import io.legado.app.service.help.Download
import io.legado.app.ui.association.ImportBookSourceActivity import io.legado.app.ui.association.ImportBookSourceActivity
import io.legado.app.ui.association.ImportReplaceRuleActivity import io.legado.app.ui.association.ImportReplaceRuleActivity
import io.legado.app.ui.association.ImportRssSourceActivity import io.legado.app.ui.association.ImportRssSourceActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
@ -33,7 +33,6 @@ import splitties.systemservices.downloadManager
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(false), class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(false),
FilePickerDialog.CallBack,
ReadRssViewModel.CallBack { ReadRssViewModel.CallBack {
override val viewModel: ReadRssViewModel override val viewModel: ReadRssViewModel
@ -44,6 +43,10 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
private var ttsMenuItem: MenuItem? = null private var ttsMenuItem: MenuItem? = null
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private var webPic: String? = null private var webPic: String? = null
private val saveImage = registerForActivityResult(FilePicker()) {
ACache.get(this).put(imagePathKey, it.toString())
viewModel.saveImage(webPic, it.toString())
}
override fun getViewBinding(): ActivityRssReadBinding { override fun getViewBinding(): ActivityRssReadBinding {
return ActivityRssReadBinding.inflate(layoutInflater) return ActivityRssReadBinding.inflate(layoutInflater)
@ -156,14 +159,11 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
if (!path.isNullOrEmpty()) { if (!path.isNullOrEmpty()) {
default.add(path) default.add(path)
} }
FilePicker.selectFolder( saveImage.launch(
this, FilePickerParam(
savePathRequestCode, otherActions = default.toTypedArray()
getString(R.string.save_image), )
default )
) {
viewModel.saveImage(webPic, it)
}
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@ -269,16 +269,6 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
savePathRequestCode -> data?.data?.let {
ACache.get(this).put(imagePathKey, it.toString())
viewModel.saveImage(webPic, it.toString())
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
binding.webView.destroy() binding.webView.destroy()

@ -1,7 +1,6 @@
package io.legado.app.ui.rss.source.edit package io.legado.app.ui.rss.source.edit
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
@ -20,7 +19,7 @@ import io.legado.app.help.LocalConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -34,9 +33,15 @@ class RssSourceEditActivity :
private var mSoftKeyboardTool: PopupWindow? = null private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false private var mIsSoftKeyBoardShowing = false
private val qrRequestCode = 101
private val adapter = RssSourceEditAdapter() private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList() private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
}
}
override fun getViewBinding(): ActivityRssSourceEditBinding { override fun getViewBinding(): ActivityRssSourceEditBinding {
return ActivityRssSourceEditBinding.inflate(layoutInflater) return ActivityRssSourceEditBinding.inflate(layoutInflater)
@ -107,7 +112,7 @@ class RssSourceEditActivity :
} }
} }
R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource())) R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
R.id.menu_qr_code_camera -> startActivityForResult<QrCodeActivity>(qrRequestCode) R.id.menu_qr_code_camera -> qrCodeResult.launch(null)
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> share(GSON.toJson(getRssSource())) R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
R.id.menu_share_qr -> shareWithQr( R.id.menu_share_qr -> shareWithQr(
@ -269,16 +274,4 @@ class RssSourceEditActivity :
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
}
}
}
}
} }

@ -1,7 +1,6 @@
package io.legado.app.ui.rss.source.manage package io.legado.app.ui.rss.source.manage
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -25,9 +24,9 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.association.ImportRssSourceActivity import io.legado.app.ui.association.ImportRssSourceActivity
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.SelectActionBar import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -37,23 +36,51 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File import java.io.File
/**
* 订阅源管理
*/
class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceViewModel>(), class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceViewModel>(),
PopupMenu.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener,
FilePickerDialog.CallBack,
SelectActionBar.CallBack, SelectActionBar.CallBack,
RssSourceAdapter.CallBack { RssSourceAdapter.CallBack {
override val viewModel: RssSourceViewModel override val viewModel: RssSourceViewModel
by viewModels() by viewModels()
private val importRecordKey = "rssSourceRecordKey" private val importRecordKey = "rssSourceRecordKey"
private val qrRequestCode = 101
private val importRequestCode = 124
private val exportRequestCode = 65
private lateinit var adapter: RssSourceAdapter private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>() private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
startActivity<ImportRssSourceActivity> {
putExtra("source", it)
}
}
private val importDoc = registerForActivityResult(FilePicker()) { uri ->
kotlin.runCatching {
uri?.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportRssSourceActivity> {
putExtra("dataKey", dataKey)
}
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
private val exportDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
override fun getViewBinding(): ActivityRssSourceBinding { override fun getViewBinding(): ActivityRssSourceBinding {
return ActivityRssSourceBinding.inflate(layoutInflater) return ActivityRssSourceBinding.inflate(layoutInflater)
@ -81,10 +108,14 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_add -> startActivity<RssSourceEditActivity>() R.id.menu_add -> startActivity<RssSourceEditActivity>()
R.id.menu_import_source_local -> FilePicker R.id.menu_import_local -> importDoc.launch(
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) FilePickerParam(
R.id.menu_import_source_onLine -> showImportDialog() mode = FilePicker.FILE,
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode) allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_qr -> qrCodeResult.launch(null)
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(supportFragmentManager, "rssGroupManage") .show(supportFragmentManager, "rssGroupManage")
R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) { R.id.menu_share_source -> viewModel.shareSelection(adapter.getSelection()) {
@ -105,7 +136,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection())
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection())
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) R.id.menu_export_selection -> exportDir.launch(null)
R.id.menu_top_sel -> viewModel.topSource(*adapter.getSelection().toTypedArray()) R.id.menu_top_sel -> viewModel.topSource(*adapter.getSelection().toTypedArray())
R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray()) R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray())
} }
@ -235,7 +266,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
.getAsString(importRecordKey) .getAsString(importRecordKey)
?.splitNotBlank(",") ?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_book_source_on_line) { alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setFilterValues(cacheUrls) editView.setFilterValues(cacheUrls)
editView.delCallBack = { editView.delCallBack = {
@ -260,46 +291,6 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
}.show() }.show()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
importRequestCode -> if (resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
kotlin.runCatching {
uri.readText(this)?.let {
val dataKey = IntentDataHelp.putData(it)
startActivity<ImportRssSourceActivity> {
putExtra("dataKey", dataKey)
}
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
}
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
startActivity<ImportRssSourceActivity> {
putExtra("source", it)
}
}
}
exportRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(this, uri)?.let {
viewModel.exportSelection(adapter.getSelection(), it)
}
} else {
uri.path?.let {
viewModel.exportSelection(adapter.getSelection(), File(it))
}
}
}
}
}
}
override fun del(source: RssSource) { override fun del(source: RssSource) {
viewModel.del(source) viewModel.del(source)
} }

@ -1,6 +1,5 @@
package io.legado.app.ui.widget.font package io.legado.app.ui.widget.font
import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -20,8 +19,8 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -32,16 +31,37 @@ import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class FontSelectDialog : BaseDialogFragment(), class FontSelectDialog : BaseDialogFragment(),
FilePickerDialog.CallBack,
Toolbar.OnMenuItemClickListener, Toolbar.OnMenuItemClickListener,
FontAdapter.CallBack { FontAdapter.CallBack {
private val fontFolderRequestCode = 35485
private val fontRegex = Regex(".*\\.[ot]tf") private val fontRegex = Regex(".*\\.[ot]tf")
private val fontFolder by lazy { private val fontFolder by lazy {
FileUtils.createFolderIfNotExist(appCtx.filesDir, "Fonts") FileUtils.createFolderIfNotExist(appCtx.filesDir, "Fonts")
} }
private var adapter: FontAdapter? = null private var adapter: FontAdapter? = null
private val binding by viewBinding(DialogFontSelectBinding::bind) private val binding by viewBinding(DialogFontSelectBinding::bind)
private val selectFontDir = registerForActivityResult(FilePicker()) { uri ->
uri ?: return@registerForActivityResult
if (uri.toString().isContentScheme()) {
putPrefString(PreferKey.fontFolder, uri.toString())
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc != null) {
context?.contentResolver?.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
loadFontFiles(doc)
} else {
RealPathUtil.getPath(requireContext(), uri)?.let {
loadFontFilesByPermission(it)
}
}
} else {
uri.path?.let { path ->
putPrefString(PreferKey.fontFolder, path)
loadFontFilesByPermission(path)
}
}
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -108,19 +128,11 @@ class FontSelectDialog : BaseDialogFragment(),
private fun openFolder() { private fun openFolder() {
launch(Main) { launch(Main) {
val defaultPath = "SD${File.separator}Fonts" val defaultPath = "SD${File.separator}Fonts"
FilePicker.selectFolder( selectFontDir.launch(
this@FontSelectDialog, FilePickerParam(
fontFolderRequestCode, otherActions = arrayOf(defaultPath)
otherActions = arrayListOf(defaultPath) )
) { )
when (it) {
defaultPath -> {
val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts"
putPrefString(PreferKey.fontFolder, path)
loadFontFilesByPermission(path)
}
}
}
} }
} }
@ -227,36 +239,6 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
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 ->
if (uri.toString().isContentScheme()) {
putPrefString(PreferKey.fontFolder, uri.toString())
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc != null) {
context?.contentResolver?.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
loadFontFiles(doc)
} else {
RealPathUtil.getPath(requireContext(), uri)?.let {
loadFontFilesByPermission(it)
}
}
} else {
uri.path?.let { path ->
putPrefString(PreferKey.fontFolder, path)
loadFontFilesByPermission(path)
}
}
}
}
}
}
private fun onDefaultFontChange() { private fun onDefaultFontChange() {
callBack?.selectFont("") callBack?.selectFont("")
} }

@ -150,7 +150,7 @@ class FastScrollRecyclerView : RecyclerView {
super.onAttachedToWindow() super.onAttachedToWindow()
mFastScroller.attachRecyclerView(this) mFastScroller.attachRecyclerView(this)
val parent = parent val parent = parent
if (parent is ViewGroup) { if (parent is ViewGroup && parent.indexOfChild(mFastScroller) == -1) {
parent.addView(mFastScroller) parent.addView(mFastScroller)
mFastScroller.setLayoutParams(parent) mFastScroller.setLayoutParams(parent)
} }

@ -152,12 +152,10 @@ class FastScroller : LinearLayout {
fun attachRecyclerView(recyclerView: RecyclerView) { fun attachRecyclerView(recyclerView: RecyclerView) {
mRecyclerView = recyclerView mRecyclerView = recyclerView
if (mRecyclerView != null) { mRecyclerView!!.addOnScrollListener(mScrollListener)
mRecyclerView!!.addOnScrollListener(mScrollListener) post {
post { // set initial positions for bubble and handle
// set initial positions for bubble and handle setViewPositions(getScrollProportion(mRecyclerView))
setViewPositions(getScrollProportion(mRecyclerView))
}
} }
} }

@ -1,20 +1,11 @@
package io.legado.app.utils package io.legado.app.utils
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowMetrics import android.view.WindowMetrics
inline fun <reified A : Activity> Activity.startActivityForResult(
requestCode: Int,
configIntent: Intent.() -> Unit = {}
) {
startActivityForResult(Intent(this, A::class.java).apply(configIntent), requestCode)
}
fun Activity.getSize(): DisplayMetrics { fun Activity.getSize(): DisplayMetrics {
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

@ -3,7 +3,7 @@ package io.legado.app.utils
import android.os.Environment import android.os.Environment
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.IntDef import androidx.annotation.IntDef
import io.legado.app.ui.filepicker.utils.ConvertUtils import io.legado.app.ui.document.utils.ConvertUtils
import splitties.init.appCtx import splitties.init.appCtx
import java.io.* import java.io.*
import java.nio.charset.Charset import java.nio.charset.Charset

@ -10,10 +10,6 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import splitties.systemservices.connectivityManager
@Suppress("DEPRECATION")
fun Fragment.isOnline() = connectivityManager.activeNetworkInfo?.isConnected == true
fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) = fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) =
requireContext().defaultSharedPreferences.getBoolean(key, defValue) requireContext().defaultSharedPreferences.getBoolean(key, defValue)
@ -64,10 +60,3 @@ inline fun <reified T : Activity> Fragment.startActivity(
) { ) {
startActivity(Intent(requireContext(), T::class.java).apply(configIntent)) startActivity(Intent(requireContext(), T::class.java).apply(configIntent))
} }
inline fun <reified T : Activity> Fragment.startActivityForResult(
requestCode: Int,
configIntent: Intent.() -> Unit = {}
) {
startActivityForResult(Intent(requireContext(), T::class.java).apply(configIntent), requestCode)
}

@ -2,13 +2,14 @@ package io.legado.app.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import java.io.File import java.io.File
fun Uri.isContentScheme() = this.scheme == "content" fun Uri.isContentScheme() = this.scheme == "content"
@Throws(Exception::class) @Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray? { fun Uri.readBytes(context: Context): ByteArray? {
if (this.toString().isContentScheme()) { if (this.isContentScheme()) {
return DocumentUtils.readBytes(context, this) return DocumentUtils.readBytes(context, this)
} else { } else {
val path = RealPathUtil.getPath(context, this) val path = RealPathUtil.getPath(context, this)
@ -32,7 +33,7 @@ fun Uri.writeBytes(
context: Context, context: Context,
byteArray: ByteArray byteArray: ByteArray
): Boolean { ): Boolean {
if (this.toString().isContentScheme()) { if (this.isContentScheme()) {
return DocumentUtils.writeBytes(context, byteArray, this) return DocumentUtils.writeBytes(context, byteArray, this)
} else { } else {
val path = RealPathUtil.getPath(context, this) val path = RealPathUtil.getPath(context, this)
@ -48,3 +49,22 @@ fun Uri.writeBytes(
fun Uri.writeText(context: Context, text: String): Boolean { fun Uri.writeText(context: Context, text: String): Boolean {
return writeBytes(context, text.toByteArray()) return writeBytes(context, text.toByteArray())
} }
fun Uri.writeBytes(
context: Context,
fileName: String,
byteArray: ByteArray
): Boolean {
if (this.isContentScheme()) {
DocumentFile.fromTreeUri(context, this)?.let { pDoc ->
DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let {
return it.uri.writeBytes(context, byteArray)
}
}
} else {
FileUtils.createFileWithReplace(path + File.separatorChar + fileName)
.writeBytes(byteArray)
return true
}
return false
}

@ -12,7 +12,7 @@
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:contentLayout="@layout/view_tab_layout" /> app:contentLayout="@layout/view_tab_layout" />
<androidx.viewpager.widget.ViewPager <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

@ -16,7 +16,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tabMode="scrollable" /> app:tabMode="scrollable" />
<androidx.viewpager.widget.ViewPager <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

@ -18,7 +18,8 @@
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

@ -85,19 +85,19 @@
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_local" android:id="@+id/menu_import_local"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_local" android:title="@string/import_local"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_onLine" android:id="@+id/menu_import_onLine"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_on_line" android:title="@string/import_on_line"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_qr" android:id="@+id/menu_import_qr"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_by_qr_code" android:title="@string/import_by_qr_code"
app:showAsAction="never" /> app:showAsAction="never" />

@ -31,19 +31,19 @@
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_local" android:id="@+id/menu_import_local"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_local" android:title="@string/import_local"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_onLine" android:id="@+id/menu_import_onLine"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_on_line" android:title="@string/import_on_line"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_qr" android:id="@+id/menu_import_qr"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_by_qr_code" android:title="@string/import_by_qr_code"
app:showAsAction="never" /> app:showAsAction="never" />

@ -31,19 +31,19 @@
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_local" android:id="@+id/menu_import_local"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_local" android:title="@string/import_local"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_onLine" android:id="@+id/menu_import_onLine"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_book_source_on_line" android:title="@string/import_on_line"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/menu_import_source_qr" android:id="@+id/menu_import_qr"
android:icon="@drawable/ic_import" android:icon="@drawable/ic_import"
android:title="@string/import_by_qr_code" android:title="@string/import_by_qr_code"
app:showAsAction="never" /> app:showAsAction="never" />

@ -15,4 +15,19 @@
android:title="@string/import_default_rule" android:title="@string/import_default_rule"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_import_local"
android:title="@string/import_local"
app:showAsAction="never" />
<item
android:id="@+id/menu_import_onLine"
android:title="@string/import_on_line"
app:showAsAction="never" />
<item
android:id="@+id/menu_export"
android:title="@string/export"
app:showAsAction="never" />
</menu> </menu>

@ -188,8 +188,8 @@
<string name="flow_sys">跟隨系統</string> <string name="flow_sys">跟隨系統</string>
<string name="add">添加</string> <string name="add">添加</string>
<string name="import_book_source">導入書源</string> <string name="import_book_source">導入書源</string>
<string name="import_book_source_local">本地導入</string> <string name="import_local">本地導入</string>
<string name="import_book_source_on_line">網絡導入</string> <string name="import_on_line">網絡導入</string>
<string name="replace_rule_title">替換淨化</string> <string name="replace_rule_title">替換淨化</string>
<string name="replace_rule_edit">替換規則編輯</string> <string name="replace_rule_edit">替換規則編輯</string>
<string name="replace_rule">替換規則</string> <string name="replace_rule">替換規則</string>

@ -190,8 +190,8 @@
<string name="flow_sys">跟隨系統</string> <string name="flow_sys">跟隨系統</string>
<string name="add">新增</string> <string name="add">新增</string>
<string name="import_book_source">匯入書源</string> <string name="import_book_source">匯入書源</string>
<string name="import_book_source_local">本機匯入</string> <string name="import_local">本機匯入</string>
<string name="import_book_source_on_line">網路匯入</string> <string name="import_on_line">網路匯入</string>
<string name="replace_rule_title">取代淨化</string> <string name="replace_rule_title">取代淨化</string>
<string name="replace_rule_edit">取代規則編輯</string> <string name="replace_rule_edit">取代規則編輯</string>
<string name="replace_rule">取代規則</string> <string name="replace_rule">取代規則</string>

@ -190,8 +190,8 @@
<string name="flow_sys">跟随系统</string> <string name="flow_sys">跟随系统</string>
<string name="add">添加</string> <string name="add">添加</string>
<string name="import_book_source">导入书源</string> <string name="import_book_source">导入书源</string>
<string name="import_book_source_local">本地导入</string> <string name="import_local">本地导入</string>
<string name="import_book_source_on_line">网络导入</string> <string name="import_on_line">网络导入</string>
<string name="replace_rule_title">替换净化</string> <string name="replace_rule_title">替换净化</string>
<string name="replace_rule_edit">替换规则编辑</string> <string name="replace_rule_edit">替换规则编辑</string>
<string name="replace_rule">替换规则</string> <string name="replace_rule">替换规则</string>

@ -191,8 +191,8 @@
<string name="flow_sys">Follow system</string> <string name="flow_sys">Follow system</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="import_book_source">Import book sources</string> <string name="import_book_source">Import book sources</string>
<string name="import_book_source_local">Import local sources</string> <string name="import_local">Import local</string>
<string name="import_book_source_on_line">Import online sources</string> <string name="import_on_line">Import online</string>
<string name="replace_rule_title">Replacement</string> <string name="replace_rule_title">Replacement</string>
<string name="replace_rule_edit">Edit replacement rule</string> <string name="replace_rule_edit">Edit replacement rule</string>
<string name="replace_rule">Pattern</string> <string name="replace_rule">Pattern</string>

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.4.31' ext.kotlin_version = '1.4.32'
repositories { repositories {
google() google()
maven { url "https://maven.aliyun.com/nexus/content/groups/public/" } maven { url "https://maven.aliyun.com/nexus/content/groups/public/" }

@ -2,7 +2,7 @@ package me.ag2s.epublib.browsersupport;
import java.util.EventObject; import java.util.EventObject;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
@ -20,9 +20,9 @@ public class NavigationEvent extends EventObject {
private Resource oldResource; private Resource oldResource;
private int oldSpinePos; private int oldSpinePos;
private Navigator navigator; private Navigator navigator;
private Book oldBook; private EpubBook oldBook;
private int oldSectionPos; private int oldSectionPos;
private String oldFragmentId; private String oldFragmentId;
public NavigationEvent(Object source) { public NavigationEvent(Object source) {
@ -61,9 +61,9 @@ public class NavigationEvent extends EventObject {
this.oldFragmentId = oldFragmentId; this.oldFragmentId = oldFragmentId;
} }
public Book getOldBook() { public EpubBook getOldBook() {
return oldBook; return oldBook;
} }
// package // package
void setOldPagePos(int oldPagePos) { void setOldPagePos(int oldPagePos) {
@ -124,13 +124,13 @@ public class NavigationEvent extends EventObject {
} }
public void setOldBook(Book oldBook) { public void setOldBook(EpubBook oldBook) {
this.oldBook = oldBook; this.oldBook = oldBook;
} }
public Book getCurrentBook() { public EpubBook getCurrentBook() {
return getNavigator().getBook(); return getNavigator().getBook();
} }
public boolean isResourceChanged() { public boolean isResourceChanged() {
return oldResource != getCurrentResource(); return oldResource != getCurrentResource();

@ -1,15 +1,15 @@
package me.ag2s.epublib.browsersupport; package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/** /**
* A history of the user's locations with the epub. * A history of the user's locations with the epub.
* *
* @author paul.siegmann * @author paul.siegmann
*
*/ */
public class NavigationHistory implements NavigationEventListener { public class NavigationHistory implements NavigationEventListener {
@ -58,7 +58,7 @@ public class NavigationHistory implements NavigationEventListener {
return currentSize; return currentSize;
} }
public void initBook(Book book) { public void initBook(EpubBook book) {
if (book == null) { if (book == null) {
return; return;
} }

@ -1,14 +1,15 @@
package me.ag2s.epublib.browsersupport; package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/** /**
* A helper class for epub browser applications. * A helper class for epub browser applications.
* * <p>
* It helps moving from one resource to the other, from one resource * It helps moving from one resource to the other, from one resource
* to the other and keeping other elements of the application up-to-date * to the other and keeping other elements of the application up-to-date
* by calling the NavigationEventListeners. * by calling the NavigationEventListeners.
@ -18,7 +19,7 @@ import java.util.List;
public class Navigator implements Serializable { public class Navigator implements Serializable {
private static final long serialVersionUID = 1076126986424925474L; private static final long serialVersionUID = 1076126986424925474L;
private Book book; private EpubBook book;
private int currentSpinePos; private int currentSpinePos;
private Resource currentResource; private Resource currentResource;
private int currentPagePos; private int currentPagePos;
@ -30,7 +31,7 @@ public class Navigator implements Serializable {
this(null); this(null);
} }
public Navigator(Book book) { public Navigator(EpubBook book) {
this.book = book; this.book = book;
this.currentSpinePos = 0; this.currentSpinePos = 0;
if (book != null) { if (book != null) {
@ -158,7 +159,7 @@ public class Navigator implements Serializable {
return gotoSpineSection(book.getSpine().size() - 1, source); return gotoSpineSection(book.getSpine().size() - 1, source);
} }
public void gotoBook(Book book, Object source) { public void gotoBook(EpubBook book, Object source) {
NavigationEvent navigationEvent = new NavigationEvent(source, this); NavigationEvent navigationEvent = new NavigationEvent(source, this);
this.book = book; this.book = book;
this.currentFragmentId = null; this.currentFragmentId = null;
@ -193,7 +194,7 @@ public class Navigator implements Serializable {
this.currentResource = book.getSpine().getResource(currentIndex); this.currentResource = book.getSpine().getResource(currentIndex);
} }
public Book getBook() { public EpubBook getBook() {
return book; return book;
} }

@ -1,323 +0,0 @@
package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Representation of a Book.
* <p>
* All resources of a Book (html, css, xml, fonts, images) are represented
* as Resources. See getResources() for access to these.<br/>
* A Book as 3 indexes into these Resources, as per the epub specification.<br/>
* <dl>
* <dt>Spine</dt>
* <dd>these are the Resources to be shown when a user reads the book from
* start to finish.</dd>
* <dt>Table of Contents<dt>
* <dd>The table of contents. Table of Contents references may be in a
* different order and contain different Resources than the spine, and often do.
* <dt>Guide</dt>
* <dd>The Guide has references to a set of special Resources like the
* cover page, the Glossary, the copyright page, etc.
* </dl>
* <p/>
* The complication is that these 3 indexes may and usually do point to
* different pages.
* A chapter may be split up in 2 pieces to fit it in to memory. Then the
* spine will contain both pieces, but the Table of Contents only the first.
* <p>
* The Content page may be in the Table of Contents, the Guide, but not
* in the Spine.
* Etc.
* <p/>
* <p>
* Please see the illustration at: doc/schema.svg
*
* @author paul
* @author jake
*/
public class Book implements Serializable {
private static final long serialVersionUID = 2068355170895770100L;
private Resources resources = new Resources();
private Metadata metadata = new Metadata();
private Spine spine = new Spine();
private TableOfContents tableOfContents = new TableOfContents();
private final Guide guide = new Guide();
private Resource opfResource;
private Resource ncxResource;
private Resource coverImage;
private String version="2.0";
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public boolean isEpub3() {
return this.version.startsWith("3.");
}
@SuppressWarnings("UnusedReturnValue")
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource) {
return addSection(parentSection, sectionTitle, resource, null);
}
/**
* Adds the resource to the table of contents of the book as a child
* section of the given parentSection
*
* @param parentSection parentSection
* @param sectionTitle sectionTitle
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource,
String fragmentId) {
getResources().add(resource);
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return parentSection.addChildSection(
new TOCReference(sectionTitle, resource, fragmentId));
}
public TOCReference addSection(String title, Resource resource) {
return addSection(title, resource, null);
}
/**
* Adds a resource to the book's set of resources, table of contents and
* if there is no resource with the id in the spine also adds it to the spine.
*
* @param title title
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
String title, Resource resource, String fragmentId) {
getResources().add(resource);
TOCReference tocReference = tableOfContents
.addTOCReference(new TOCReference(title, resource, fragmentId));
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return tocReference;
}
@SuppressWarnings("unused")
public void generateSpineFromTableOfContents() {
Spine spine = new Spine(tableOfContents);
// in case the tocResource was already found and assigned
spine.setTocResource(this.spine.getTocResource());
this.spine = spine;
}
/**
* The Book's metadata (titles, authors, etc)
*
* @return The Book's metadata (titles, authors, etc)
*/
public Metadata getMetadata() {
return metadata;
}
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
}
public void setResources(Resources resources) {
this.resources = resources;
}
@SuppressWarnings("unused")
public Resource addResource(Resource resource) {
return resources.add(resource);
}
/**
* The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*
* @return The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*/
public Resources getResources() {
return resources;
}
/**
* The sections of the book that should be shown if a user reads the book
* from start to finish.
*
* @return The Spine
*/
public Spine getSpine() {
return spine;
}
public void setSpine(Spine spine) {
this.spine = spine;
}
/**
* The Table of Contents of the book.
*
* @return The Table of Contents of the book.
*/
public TableOfContents getTableOfContents() {
return tableOfContents;
}
public void setTableOfContents(TableOfContents tableOfContents) {
this.tableOfContents = tableOfContents;
}
/**
* The book's cover page as a Resource.
* An XHTML document containing a link to the cover image.
*
* @return The book's cover page as a Resource
*/
public Resource getCoverPage() {
Resource coverPage = guide.getCoverPage();
if (coverPage == null) {
coverPage = spine.getResource(0);
}
return coverPage;
}
public void setCoverPage(Resource coverPage) {
if (coverPage == null) {
return;
}
if (resources.notContainsByHref(coverPage.getHref())) {
resources.add(coverPage);
}
guide.setCoverPage(coverPage);
}
/**
* Gets the first non-blank title from the book's metadata.
*
* @return the first non-blank title from the book's metadata.
*/
public String getTitle() {
return getMetadata().getFirstTitle();
}
/**
* The book's cover image.
*
* @return The book's cover image.
*/
public Resource getCoverImage() {
return coverImage;
}
public void setCoverImage(Resource coverImage) {
if (coverImage == null) {
return;
}
if (resources.notContainsByHref(coverImage.getHref())) {
resources.add(coverImage);
}
this.coverImage = coverImage;
}
/**
* The guide; contains references to special sections of the book like
* colophon, glossary, etc.
*
* @return The guide; contains references to special sections of the book
* like colophon, glossary, etc.
*/
public Guide getGuide() {
return guide;
}
/**
* All Resources of the Book that can be reached via the Spine, the
* TableOfContents or the Guide.
* <p/>
* Consists of a list of "reachable" resources:
* <ul>
* <li>The coverpage</li>
* <li>The resources of the Spine that are not already in the result</li>
* <li>The resources of the Table of Contents that are not already in the
* result</li>
* <li>The resources of the Guide that are not already in the result</li>
* </ul>
* To get all html files that make up the epub file use
* {@link #getResources()}
*
* @return All Resources of the Book that can be reached via the Spine,
* the TableOfContents or the Guide.
*/
public List<Resource> getContents() {
Map<String, Resource> result = new LinkedHashMap<>();
addToContentsResult(getCoverPage(), result);
for (SpineReference spineReference : getSpine().getSpineReferences()) {
addToContentsResult(spineReference.getResource(), result);
}
for (Resource resource : getTableOfContents().getAllUniqueResources()) {
addToContentsResult(resource, result);
}
for (GuideReference guideReference : getGuide().getReferences()) {
addToContentsResult(guideReference.getResource(), result);
}
return new ArrayList<>(result.values());
}
private static void addToContentsResult(Resource resource,
Map<String, Resource> allReachableResources) {
if (resource != null && (!allReachableResources
.containsKey(resource.getHref()))) {
allReachableResources.put(resource.getHref(), resource);
}
}
public Resource getOpfResource() {
return opfResource;
}
public void setOpfResource(Resource opfResource) {
this.opfResource = opfResource;
}
public void setNcxResource(Resource ncxResource) {
this.ncxResource = ncxResource;
}
public Resource getNcxResource() {
return ncxResource;
}
}

@ -1,9 +1,323 @@
package me.ag2s.epublib.domain; package me.ag2s.epublib.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** /**
* 这个是用于其它与Book类同名时替换的 * Representation of a Book.
* <p>
* All resources of a Book (html, css, xml, fonts, images) are represented
* as Resources. See getResources() for access to these.<br/>
* A Book as 3 indexes into these Resources, as per the epub specification.<br/>
* <dl>
* <dt>Spine</dt>
* <dd>these are the Resources to be shown when a user reads the book from
* start to finish.</dd>
* <dt>Table of Contents<dt>
* <dd>The table of contents. Table of Contents references may be in a
* different order and contain different Resources than the spine, and often do.
* <dt>Guide</dt>
* <dd>The Guide has references to a set of special Resources like the
* cover page, the Glossary, the copyright page, etc.
* </dl>
* <p/>
* The complication is that these 3 indexes may and usually do point to
* different pages.
* A chapter may be split up in 2 pieces to fit it in to memory. Then the
* spine will contain both pieces, but the Table of Contents only the first.
* <p>
* The Content page may be in the Table of Contents, the Guide, but not
* in the Spine.
* Etc.
* <p/>
* <p>
* Please see the illustration at: doc/schema.svg
*
* @author paul
* @author jake
*/ */
@SuppressWarnings("unused declaration") public class EpubBook implements Serializable {
public class EpubBook extends Book {
private static final long serialVersionUID = 2068355170895770100L;
private Resources resources = new Resources();
private Metadata metadata = new Metadata();
private Spine spine = new Spine();
private TableOfContents tableOfContents = new TableOfContents();
private final Guide guide = new Guide();
private Resource opfResource;
private Resource ncxResource;
private Resource coverImage;
private String version = "2.0";
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public boolean isEpub3() {
return this.version.startsWith("3.");
}
@SuppressWarnings("UnusedReturnValue")
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource) {
return addSection(parentSection, sectionTitle, resource, null);
}
/**
* Adds the resource to the table of contents of the book as a child
* section of the given parentSection
*
* @param parentSection parentSection
* @param sectionTitle sectionTitle
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
TOCReference parentSection, String sectionTitle, Resource resource,
String fragmentId) {
getResources().add(resource);
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return parentSection.addChildSection(
new TOCReference(sectionTitle, resource, fragmentId));
}
public TOCReference addSection(String title, Resource resource) {
return addSection(title, resource, null);
}
/**
* Adds a resource to the book's set of resources, table of contents and
* if there is no resource with the id in the spine also adds it to the spine.
*
* @param title title
* @param resource resource
* @param fragmentId fragmentId
* @return The table of contents
*/
public TOCReference addSection(
String title, Resource resource, String fragmentId) {
getResources().add(resource);
TOCReference tocReference = tableOfContents
.addTOCReference(new TOCReference(title, resource, fragmentId));
if (spine.findFirstResourceById(resource.getId()) < 0) {
spine.addSpineReference(new SpineReference(resource));
}
return tocReference;
}
@SuppressWarnings("unused")
public void generateSpineFromTableOfContents() {
Spine spine = new Spine(tableOfContents);
// in case the tocResource was already found and assigned
spine.setTocResource(this.spine.getTocResource());
this.spine = spine;
}
/**
* The Book's metadata (titles, authors, etc)
*
* @return The Book's metadata (titles, authors, etc)
*/
public Metadata getMetadata() {
return metadata;
}
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
}
public void setResources(Resources resources) {
this.resources = resources;
}
@SuppressWarnings("unused")
public Resource addResource(Resource resource) {
return resources.add(resource);
}
/**
* The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*
* @return The collection of all images, chapters, sections, xhtml files,
* stylesheets, etc that make up the book.
*/
public Resources getResources() {
return resources;
}
/**
* The sections of the book that should be shown if a user reads the book
* from start to finish.
*
* @return The Spine
*/
public Spine getSpine() {
return spine;
}
public void setSpine(Spine spine) {
this.spine = spine;
}
/**
* The Table of Contents of the book.
*
* @return The Table of Contents of the book.
*/
public TableOfContents getTableOfContents() {
return tableOfContents;
}
public void setTableOfContents(TableOfContents tableOfContents) {
this.tableOfContents = tableOfContents;
}
/**
* The book's cover page as a Resource.
* An XHTML document containing a link to the cover image.
*
* @return The book's cover page as a Resource
*/
public Resource getCoverPage() {
Resource coverPage = guide.getCoverPage();
if (coverPage == null) {
coverPage = spine.getResource(0);
}
return coverPage;
}
public void setCoverPage(Resource coverPage) {
if (coverPage == null) {
return;
}
if (resources.notContainsByHref(coverPage.getHref())) {
resources.add(coverPage);
}
guide.setCoverPage(coverPage);
}
/**
* Gets the first non-blank title from the book's metadata.
*
* @return the first non-blank title from the book's metadata.
*/
public String getTitle() {
return getMetadata().getFirstTitle();
}
/**
* The book's cover image.
*
* @return The book's cover image.
*/
public Resource getCoverImage() {
return coverImage;
}
public void setCoverImage(Resource coverImage) {
if (coverImage == null) {
return;
}
if (resources.notContainsByHref(coverImage.getHref())) {
resources.add(coverImage);
}
this.coverImage = coverImage;
}
/**
* The guide; contains references to special sections of the book like
* colophon, glossary, etc.
*
* @return The guide; contains references to special sections of the book
* like colophon, glossary, etc.
*/
public Guide getGuide() {
return guide;
}
/**
* All Resources of the Book that can be reached via the Spine, the
* TableOfContents or the Guide.
* <p/>
* Consists of a list of "reachable" resources:
* <ul>
* <li>The coverpage</li>
* <li>The resources of the Spine that are not already in the result</li>
* <li>The resources of the Table of Contents that are not already in the
* result</li>
* <li>The resources of the Guide that are not already in the result</li>
* </ul>
* To get all html files that make up the epub file use
* {@link #getResources()}
*
* @return All Resources of the Book that can be reached via the Spine,
* the TableOfContents or the Guide.
*/
public List<Resource> getContents() {
Map<String, Resource> result = new LinkedHashMap<>();
addToContentsResult(getCoverPage(), result);
for (SpineReference spineReference : getSpine().getSpineReferences()) {
addToContentsResult(spineReference.getResource(), result);
}
for (Resource resource : getTableOfContents().getAllUniqueResources()) {
addToContentsResult(resource, result);
}
for (GuideReference guideReference : getGuide().getReferences()) {
addToContentsResult(guideReference.getResource(), result);
}
return new ArrayList<>(result.values());
}
private static void addToContentsResult(Resource resource,
Map<String, Resource> allReachableResources) {
if (resource != null && (!allReachableResources
.containsKey(resource.getHref()))) {
allReachableResources.put(resource.getHref(), resource);
}
}
public Resource getOpfResource() {
return opfResource;
}
public void setOpfResource(Resource opfResource) {
this.opfResource = opfResource;
}
public void setNcxResource(Resource ncxResource) {
this.ncxResource = ncxResource;
}
public Resource getNcxResource() {
return ncxResource;
}
} }

@ -1,6 +1,6 @@
package me.ag2s.epublib.epub; package me.ag2s.epublib.epub;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
/** /**
* Post-processes a book. * Post-processes a book.
@ -16,5 +16,5 @@ public interface BookProcessor {
*/ */
BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; BookProcessor IDENTITY_BOOKPROCESSOR = book -> book;
Book processBook(Book book); EpubBook processBook(EpubBook book);
} }

@ -2,14 +2,15 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import me.ag2s.epublib.domain.Book;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
/** /**
* A book processor that combines several other bookprocessors * A book processor that combines several other bookprocessors
* * <p>
* Fixes coverpage/coverimage. * Fixes coverpage/coverimage.
* Cleans up the XHTML. * Cleans up the XHTML.
* *
@ -30,7 +31,7 @@ public class BookProcessorPipeline implements BookProcessor {
} }
@Override @Override
public Book processBook(Book book) { public EpubBook processBook(EpubBook book) {
if (bookProcessors == null) { if (bookProcessors == null) {
return book; return book;
} }
@ -38,7 +39,7 @@ public class BookProcessorPipeline implements BookProcessor {
try { try {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG,e.getMessage(), e); Log.e(TAG, e.getMessage(), e);
} }
} }
return book; return book;

@ -13,7 +13,6 @@ import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
@ -27,22 +26,21 @@ import me.ag2s.epublib.util.StringUtil;
* *
* @author paul * @author paul
*/ */
@SuppressWarnings("ALL")
public class EpubReader { public class EpubReader {
private static final String TAG = EpubReader.class.getName(); private static final String TAG = EpubReader.class.getName();
private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR;
@SuppressWarnings("unused")
public Book readEpub(InputStream in) throws IOException { public EpubBook readEpub(InputStream in) throws IOException {
return readEpub(in, Constants.CHARACTER_ENCODING); return readEpub(in, Constants.CHARACTER_ENCODING);
} }
@SuppressWarnings("unused") public EpubBook readEpub(ZipInputStream in) throws IOException {
public Book readEpub(ZipInputStream in) throws IOException {
return readEpub(in, Constants.CHARACTER_ENCODING); return readEpub(in, Constants.CHARACTER_ENCODING);
} }
@SuppressWarnings("unused") public EpubBook readEpub(ZipFile zipfile) throws IOException {
public Book readEpub(ZipFile zipfile) throws IOException {
return readEpub(zipfile, Constants.CHARACTER_ENCODING); return readEpub(zipfile, Constants.CHARACTER_ENCODING);
} }
@ -54,7 +52,7 @@ public class EpubReader {
* @return the Book as read from the inputstream * @return the Book as read from the inputstream
* @throws IOException IOException * @throws IOException IOException
*/ */
public Book readEpub(InputStream in, String encoding) throws IOException { public EpubBook readEpub(InputStream in, String encoding) throws IOException {
return readEpub(new ZipInputStream(in), encoding); return readEpub(new ZipInputStream(in), encoding);
} }
@ -67,18 +65,17 @@ public class EpubReader {
* @return this Book without loading all resources into memory. * @return this Book without loading all resources into memory.
* @throws IOException IOException * @throws IOException IOException
*/ */
@SuppressWarnings("unused") public EpubBook readEpubLazy(ZipFile zipFile, String encoding)
public Book readEpubLazy(ZipFile zipFile, String encoding)
throws IOException { throws IOException {
return readEpubLazy(zipFile, encoding, return readEpubLazy(zipFile, encoding,
Arrays.asList(MediaTypes.mediaTypes)); Arrays.asList(MediaTypes.mediaTypes));
} }
public Book readEpub(ZipInputStream in, String encoding) throws IOException { public EpubBook readEpub(ZipInputStream in, String encoding) throws IOException {
return readEpub(ResourcesLoader.loadResources(in, encoding)); return readEpub(ResourcesLoader.loadResources(in, encoding));
} }
public Book readEpub(ZipFile in, String encoding) throws IOException { public EpubBook readEpub(ZipFile in, String encoding) throws IOException {
return readEpub(ResourcesLoader.loadResources(in, encoding)); return readEpub(ResourcesLoader.loadResources(in, encoding));
} }
@ -91,24 +88,20 @@ public class EpubReader {
* @return this Book without loading all resources into memory. * @return this Book without loading all resources into memory.
* @throws IOException IOException * @throws IOException IOException
*/ */
public Book readEpubLazy(ZipFile zipFile, String encoding, public EpubBook readEpubLazy(ZipFile zipFile, String encoding,
List<MediaType> lazyLoadedTypes) throws IOException { List<MediaType> lazyLoadedTypes) throws IOException {
Resources resources = ResourcesLoader Resources resources = ResourcesLoader
.loadResources(zipFile, encoding, lazyLoadedTypes); .loadResources(zipFile, encoding, lazyLoadedTypes);
return readEpub(resources); return readEpub(resources);
} }
@SuppressWarnings("unused") public EpubBook readEpub(Resources resources) {
public Book readEpub(Resources resources) { return readEpub(resources, new EpubBook());
return readEpub(resources, new Book());
}
public EpubBook readEpubBook(Resources resources) {
return (EpubBook) readEpub(resources, new Book());
} }
public Book readEpub(Resources resources, Book result) { public EpubBook readEpub(Resources resources, EpubBook result) {
if (result == null) { if (result == null) {
result = new Book(); result = new EpubBook();
} }
handleMimeType(result, resources); handleMimeType(result, resources);
String packageResourceHref = getPackageResourceHref(resources); String packageResourceHref = getPackageResourceHref(resources);
@ -122,14 +115,14 @@ public class EpubReader {
} }
private Book postProcessBook(Book book) { private EpubBook postProcessBook(EpubBook book) {
if (bookProcessor != null) { if (bookProcessor != null) {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} }
return book; return book;
} }
private Resource processNcxResource(Resource packageResource, Book book) { private Resource processNcxResource(Resource packageResource, EpubBook book) {
Log.d(TAG, "OPF:getHref()" + packageResource.getHref()); Log.d(TAG, "OPF:getHref()" + packageResource.getHref());
if (book.isEpub3()) { if (book.isEpub3()) {
return NCXDocumentV3.read(book, this); return NCXDocumentV3.read(book, this);
@ -139,7 +132,7 @@ public class EpubReader {
} }
private Resource processPackageResource(String packageResourceHref, Book book, private Resource processPackageResource(String packageResourceHref, EpubBook book,
Resources resources) { Resources resources) {
Resource packageResource = resources.remove(packageResourceHref); Resource packageResource = resources.remove(packageResourceHref);
try { try {
@ -173,8 +166,7 @@ public class EpubReader {
return result; return result;
} }
@SuppressWarnings("unused") private void handleMimeType(EpubBook result, Resources resources) {
private void handleMimeType(Book result, Resources resources) {
resources.remove("mimetype"); resources.remove("mimetype");
//result.setResources(resources); //result.setResources(resources);
} }

@ -2,10 +2,8 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import me.ag2s.epublib.domain.Book; import org.xmlpull.v1.XmlSerializer;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.IOUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -14,7 +12,11 @@ import java.io.Writer;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.xmlpull.v1.XmlSerializer;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.IOUtil;
/** /**
* Generates an epub file. Not thread-safe, single use object. * Generates an epub file. Not thread-safe, single use object.
@ -40,7 +42,7 @@ public class EpubWriter {
} }
public void write(Book book, OutputStream out) throws IOException { public void write(EpubBook book, OutputStream out) throws IOException {
book = processBook(book); book = processBook(book);
ZipOutputStream resultStream = new ZipOutputStream(out); ZipOutputStream resultStream = new ZipOutputStream(out);
writeMimeType(resultStream); writeMimeType(resultStream);
@ -51,19 +53,19 @@ public class EpubWriter {
resultStream.close(); resultStream.close();
} }
private Book processBook(Book book) { private EpubBook processBook(EpubBook book) {
if (bookProcessor != null) { if (bookProcessor != null) {
book = bookProcessor.processBook(book); book = bookProcessor.processBook(book);
} }
return book; return book;
} }
private void initTOCResource(Book book) { private void initTOCResource(EpubBook book) {
Resource tocResource; Resource tocResource;
try { try {
if(book.isEpub3()){ if (book.isEpub3()) {
tocResource = NCXDocumentV3.createNCXResource(book); tocResource = NCXDocumentV3.createNCXResource(book);
}else { } else {
tocResource = NCXDocumentV2.createNCXResource(book); tocResource = NCXDocumentV2.createNCXResource(book);
} }
@ -81,7 +83,7 @@ public class EpubWriter {
} }
private void writeResources(Book book, ZipOutputStream resultStream) { private void writeResources(EpubBook book, ZipOutputStream resultStream) {
for (Resource resource : book.getResources().getAll()) { for (Resource resource : book.getResources().getAll()) {
writeResource(resource, resultStream); writeResource(resource, resultStream);
} }
@ -108,11 +110,11 @@ public class EpubWriter {
} }
private void writePackageDocument(Book book, ZipOutputStream resultStream) private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)
throws IOException { throws IOException {
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
XmlSerializer xmlSerializer = EpubProcessorSupport XmlSerializer xmlSerializer = EpubProcessorSupport
.createXmlSerializer(resultStream); .createXmlSerializer(resultStream);
PackageDocumentWriter.write(this, xmlSerializer, book); PackageDocumentWriter.write(this, xmlSerializer, book);
xmlSerializer.flush(); xmlSerializer.flush();
// String resultAsString = result.toString(); // String resultAsString = result.toString();

@ -19,7 +19,7 @@ import java.util.zip.ZipOutputStream;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resource;
@ -77,7 +77,7 @@ public class NCXDocumentV2 {
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static Resource read(Book book, EpubReader epubReader) { public static Resource read(EpubBook book, EpubReader epubReader) {
Resource ncxResource = null; Resource ncxResource = null;
if (book.getSpine().getTocResource() == null) { if (book.getSpine().getTocResource() == null) {
Log.e(TAG, "Book does not contain a table of contents file"); Log.e(TAG, "Book does not contain a table of contents file");
@ -107,7 +107,7 @@ public class NCXDocumentV2 {
} }
static List<TOCReference> readTOCReferences(NodeList navpoints, static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) { EpubBook book) {
if (navpoints == null) { if (navpoints == null) {
return new ArrayList<>(); return new ArrayList<>();
} }
@ -127,7 +127,7 @@ public class NCXDocumentV2 {
return result; return result;
} }
static TOCReference readTOCReference(Element navpointElement, Book book) { static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
String label = readNavLabel(navpointElement); String label = readNavLabel(navpointElement);
//Log.d(TAG,"label:"+label); //Log.d(TAG,"label:"+label);
String tocResourceRoot = StringUtil String tocResourceRoot = StringUtil
@ -184,8 +184,9 @@ public class NCXDocumentV2 {
return DOMUtil.getTextChildrenContent(DOMUtil return DOMUtil.getTextChildrenContent(DOMUtil
.getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text)); .getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text));
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static void write(EpubWriter epubWriter, Book book, public static void write(EpubWriter epubWriter, EpubBook book,
ZipOutputStream resultStream) throws IOException { ZipOutputStream resultStream) throws IOException {
resultStream resultStream
.putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));
@ -200,17 +201,17 @@ public class NCXDocumentV2 {
* *
* @param xmlSerializer the serializer used * @param xmlSerializer the serializer used
* @param book the book to serialize * @param book the book to serialize
* @throws IOException IOException * @throws IOException IOException
* @throws IllegalStateException IllegalStateException * @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException * @throws IllegalArgumentException IllegalArgumentException
*/ */
public static void write(XmlSerializer xmlSerializer, Book book) public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents()); book.getMetadata().getAuthors(), book.getTableOfContents());
} }
public static Resource createNCXResource(Book book) public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(), return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(), book.getTitle(), book.getMetadata().getAuthors(),

@ -17,7 +17,7 @@ import java.util.List;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaType;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
@ -96,7 +96,7 @@ public class NCXDocumentV3 {
* @return Resource * @return Resource
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static Resource read(Book book, EpubReader epubReader) { public static Resource read(EpubBook book, EpubReader epubReader) {
Resource ncxResource = null; Resource ncxResource = null;
if (book.getSpine().getTocResource() == null) { if (book.getSpine().getTocResource() == null) {
Log.e(TAG, "Book does not contain a table of contents file"); Log.e(TAG, "Book does not contain a table of contents file");
@ -126,7 +126,7 @@ public class NCXDocumentV3 {
return ncxResource; return ncxResource;
} }
private static List<TOCReference> doToc(Node n, Book book) { private static List<TOCReference> doToc(Node n, EpubBook book) {
List<TOCReference> result = new ArrayList<>(); List<TOCReference> result = new ArrayList<>();
if (n == null || n.getNodeType() != Document.ELEMENT_NODE) { if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
@ -143,7 +143,7 @@ public class NCXDocumentV3 {
static List<TOCReference> readTOCReferences(NodeList navpoints, static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) { EpubBook book) {
if (navpoints == null) { if (navpoints == null) {
return new ArrayList<>(); return new ArrayList<>();
} }
@ -169,7 +169,7 @@ public class NCXDocumentV3 {
} }
static TOCReference readTOCReference(Element navpointElement, Book book) { static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
//章节的名称 //章节的名称
String label = readNavLabel(navpointElement); String label = readNavLabel(navpointElement);
//Log.d(TAG, "label:" + label); //Log.d(TAG, "label:" + label);
@ -260,7 +260,7 @@ public class NCXDocumentV3 {
} }
public static Resource createNCXResource(Book book) public static Resource createNCXResource(EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
return createNCXResource(book.getMetadata().getIdentifiers(), return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(), book.getTitle(), book.getMetadata().getAuthors(),
@ -289,7 +289,7 @@ public class NCXDocumentV3 {
* @throws IllegalStateException IllegalStateException * @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException * @throws IllegalArgumentException IllegalArgumentException
*/ */
public static void write(XmlSerializer xmlSerializer, Book book) public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents()); book.getMetadata().getAuthors(), book.getTableOfContents());

@ -1,39 +1,42 @@
package me.ag2s.epublib.epub; package me.ag2s.epublib.epub;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Author; import me.ag2s.epublib.domain.Author;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Date; import me.ag2s.epublib.domain.Date;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier; import me.ag2s.epublib.domain.Identifier;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import org.xmlpull.v1.XmlSerializer;
public class PackageDocumentMetadataWriter extends PackageDocumentBase { public class PackageDocumentMetadataWriter extends PackageDocumentBase {
/** /**
* Writes the book's metadata. * Writes the book's metadata.
* *
* @param book book * @param book book
* @param serializer serializer * @param serializer serializer
* @throws IOException IOException * @throws IOException IOException
* @throws IllegalStateException IllegalStateException * @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException * @throws IllegalArgumentException IllegalArgumentException
*/ */
public static void writeMetaData(Book book, XmlSerializer serializer) public static void writeMetaData(EpubBook book, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
writeIdentifiers(book.getMetadata().getIdentifiers(), serializer); writeIdentifiers(book.getMetadata().getIdentifiers(), serializer);
writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(), writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(),
serializer); serializer);
writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(), writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(),
serializer); serializer);
writeSimpleMetdataElements(DCTags.description, writeSimpleMetdataElements(DCTags.description,
book.getMetadata().getDescriptions(), serializer); book.getMetadata().getDescriptions(), serializer);
writeSimpleMetdataElements(DCTags.publisher, writeSimpleMetdataElements(DCTags.publisher,

@ -20,7 +20,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.GuideReference;
import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaType;
@ -46,8 +46,8 @@ public class PackageDocumentReader extends PackageDocumentBase {
public static void read( public static void read(
Resource packageResource, EpubReader epubReader, Book book, Resource packageResource, EpubReader epubReader, EpubBook book,
Resources resources) Resources resources)
throws SAXException, IOException { throws SAXException, IOException {
Document packageDocument = ResourceUtil.getAsDocument(packageResource); Document packageDocument = ResourceUtil.getAsDocument(packageResource);
String packageHref = packageResource.getHref(); String packageHref = packageResource.getHref();
@ -155,17 +155,17 @@ public class PackageDocumentReader extends PackageDocumentBase {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void readGuide(Document packageDocument, private static void readGuide(Document packageDocument,
EpubReader epubReader, Book book, Resources resources) { EpubReader epubReader, EpubBook book, Resources resources) {
Element guideElement = DOMUtil Element guideElement = DOMUtil
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(), .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
NAMESPACE_OPF, OPFTags.guide); NAMESPACE_OPF, OPFTags.guide);
if (guideElement == null) { if (guideElement == null) {
return; return;
} }
Guide guide = book.getGuide(); Guide guide = book.getGuide();
NodeList guideReferences = guideElement NodeList guideReferences = guideElement
.getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference);
for (int i = 0; i < guideReferences.getLength(); i++) { for (int i = 0; i < guideReferences.getLength(); i++) {
Element referenceElement = (Element) guideReferences.item(i); Element referenceElement = (Element) guideReferences.item(i);
String resourceHref = DOMUtil String resourceHref = DOMUtil
.getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href); .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href);
@ -400,23 +400,24 @@ public class PackageDocumentReader extends PackageDocumentBase {
return result; return result;
} }
/** /**
* Finds the cover resource in the packageDocument and adds it to the book if found. * Finds the cover resource in the packageDocument and adds it to the book if found.
* Keeps the cover resource in the resources map * Keeps the cover resource in the resources map
* @param packageDocument s *
* @param book x * @param packageDocument s
*/ * @param book x
private static void readCover(Document packageDocument, Book book) { */
private static void readCover(Document packageDocument, EpubBook book) {
Collection<String> coverHrefs = findCoverHrefs(packageDocument);
for (String coverHref : coverHrefs) { Collection<String> coverHrefs = findCoverHrefs(packageDocument);
Resource resource = book.getResources().getByHref(coverHref); for (String coverHref : coverHrefs) {
if (resource == null) { Resource resource = book.getResources().getByHref(coverHref);
Log.e(TAG,"Cover resource " + coverHref + " not found"); if (resource == null) {
continue; Log.e(TAG, "Cover resource " + coverHref + " not found");
} continue;
if (resource.getMediaType() == MediaTypes.XHTML) { }
book.setCoverPage(resource); if (resource.getMediaType() == MediaTypes.XHTML) {
book.setCoverPage(resource);
} else if (MediaTypes.isBitmapImage(resource.getMediaType())) { } else if (MediaTypes.isBitmapImage(resource.getMediaType())) {
book.setCoverImage(resource); book.setCoverImage(resource);
} }

@ -2,8 +2,15 @@ package me.ag2s.epublib.epub;
import android.util.Log; import android.util.Log;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.ag2s.epublib.Constants; import me.ag2s.epublib.Constants;
import me.ag2s.epublib.domain.Book; import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.GuideReference;
import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.MediaTypes;
@ -12,13 +19,6 @@ import me.ag2s.epublib.domain.Spine;
import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.domain.SpineReference;
import me.ag2s.epublib.util.StringUtil; import me.ag2s.epublib.util.StringUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.xmlpull.v1.XmlSerializer;
/** /**
* Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf
* *
@ -29,7 +29,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
private static final String TAG = PackageDocumentWriter.class.getName(); private static final String TAG = PackageDocumentWriter.class.getName();
public static void write(EpubWriter epubWriter, XmlSerializer serializer, public static void write(EpubWriter epubWriter, XmlSerializer serializer,
Book book) { EpubBook book) {
try { try {
serializer.startDocument(Constants.CHARACTER_ENCODING, false); serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
@ -66,7 +66,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
* @throws IllegalArgumentException 1@throws XMLStreamException * @throws IllegalArgumentException 1@throws XMLStreamException
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void writeSpine(Book book, EpubWriter epubWriter, private static void writeSpine(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.spine); serializer.startTag(NAMESPACE_OPF, OPFTags.spine);
@ -93,7 +93,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
} }
private static void writeManifest(Book book, EpubWriter epubWriter, private static void writeManifest(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.manifest); serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);
@ -102,7 +102,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
//For EPUB3 //For EPUB3
if (book.isEpub3()) { if (book.isEpub3()) {
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties,NCXDocumentV3.V3_NCX_PROPERTIES); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF);
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName()); serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName());
@ -124,7 +124,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
serializer.endTag(NAMESPACE_OPF, OPFTags.manifest); serializer.endTag(NAMESPACE_OPF, OPFTags.manifest);
} }
private static List<Resource> getAllResourcesSortById(Book book) { private static List<Resource> getAllResourcesSortById(EpubBook book) {
List<Resource> allResources = new ArrayList<>( List<Resource> allResources = new ArrayList<>(
book.getResources().getAll()); book.getResources().getAll());
Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId())); Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId()));
@ -134,13 +134,13 @@ public class PackageDocumentWriter extends PackageDocumentBase {
/** /**
* Writes a resources as an item element * Writes a resources as an item element
* *
* @param resource g * @param resource g
* @param serializer g * @param serializer g
* @throws IOException g * @throws IOException g
* @throws IllegalStateException g * @throws IllegalStateException g
* @throws IllegalArgumentException 1@throws XMLStreamException * @throws IllegalArgumentException 1@throws XMLStreamException
*/ */
private static void writeItem(Book book, Resource resource, private static void writeItem(EpubBook book, Resource resource,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
if (resource == null || if (resource == null ||
@ -204,7 +204,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
} }
} }
private static void writeGuide(Book book, EpubWriter epubWriter, private static void writeGuide(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer) XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.guide); serializer.startTag(NAMESPACE_OPF, OPFTags.guide);

Loading…
Cancel
Save