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:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 选择文件 -->
<activity
android:name=".ui.document.FilePickerActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme.Transparent" />
<!-- 文字处理 -->
<activity
android:name=".receiver.SharedReceiverActivity"

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

@ -154,7 +154,9 @@ abstract class BaseActivity<VB : ViewBinding>(
}
if (AppConfig.isGooglePlay) {
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() {
private val startSettingActivity =
private val settingActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
RequestPlugins.sRequestCallback?.onSettingActivityResult()
finish()
@ -37,7 +37,7 @@ class PermissionActivity : AppCompatActivity() {
-> try {
val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
settingIntent.data = Uri.fromParts("package", packageName, null)
startSettingActivity.launch(settingIntent)
settingActivityResult.launch(settingIntent)
} catch (e: Exception) {
toastOnUi(R.string.tip_cannot_jump_setting_page)
finish()

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

@ -1,7 +1,6 @@
package io.legado.app.ui.audio
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable
import android.icu.text.SimpleDateFormat
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.service.help.AudioPlay
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.seekbar.SeekBarChangeListener
import io.legado.app.utils.*
@ -42,7 +41,6 @@ class AudioPlayActivity :
override val viewModel: AudioPlayViewModel
by viewModels()
private var requestCodeChapter = 8461
private var adjustProgress = false
private val progressTimeFormat by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -51,6 +49,13 @@ class AudioPlayActivity :
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 {
return ActivityAudioPlayBinding.inflate(layoutInflater)
@ -107,9 +112,7 @@ class AudioPlayActivity :
})
binding.ivChapter.setOnClickListener {
AudioPlay.book?.let {
startActivityForResult<ChapterListActivity>(requestCodeChapter) {
putExtra("bookUrl", it.bookUrl)
}
tocActivityResult.launch(it.bookUrl)
}
}
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() {
observeEvent<Boolean>(EventBus.MEDIA_BUTTON) {
if (it) {

@ -1,6 +1,5 @@
package io.legado.app.ui.book.cache
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
@ -25,8 +24,8 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.lib.dialogs.alert
import io.legado.app.service.help.CacheBook
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.widget.dialog.TextListDialog
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers
@ -36,11 +35,25 @@ import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>(),
FilePickerDialog.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"
lateinit var adapter: CacheAdapter
private var groupLiveData: LiveData<List<BookGroup>>? = null
@ -215,9 +228,11 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
if (!path.isNullOrEmpty()) {
default.add(path)
}
FilePicker.selectFolder(this, exportRequestCode, otherActions = default) {
startExport(it)
}
exportDir.launch(
FilePickerParam(
otherActions = default.toTypedArray()
)
)
}
private fun startExport(path: String) {
@ -244,27 +259,4 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}.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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
@ -9,6 +8,7 @@ import android.view.Menu
import android.view.MenuItem
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import com.bumptech.glide.RequestBuilder
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.search.SearchActivity
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.utils.*
@ -47,9 +47,37 @@ class BookInfoActivity :
ChangeSourceDialog.CallBack,
ChangeCoverDialog.CallBack {
private val requestCodeChapterList = 568
private val requestCodeInfoEdit = 562
private val requestCodeRead = 432
private val tocActivityResult = registerForActivityResult(TocActivityResult()) {
it?.let {
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()
@ -82,11 +110,10 @@ class BookInfoActivity :
R.id.menu_edit -> {
if (viewModel.inBookshelf) {
viewModel.bookData.value?.let {
startActivityForResult<BookInfoEditActivity>(
requestCodeInfoEdit
) {
putExtra("bookUrl", it.bookUrl)
}
infoEditResult.launch(
Intent(this, BookInfoEditActivity::class.java)
.putExtra("bookUrl", it.bookUrl)
)
}
} else {
toastOnUi(R.string.after_add_bookshelf)
@ -303,11 +330,7 @@ class BookInfoActivity :
return
}
viewModel.bookData.value?.let {
startActivityForResult<ChapterListActivity>(
requestCodeChapterList
) {
putExtra("bookUrl", it.bookUrl)
}
tocActivityResult.launch(it.bookUrl)
}
}
@ -327,19 +350,17 @@ class BookInfoActivity :
private fun startReadActivity(book: Book) {
when (book.type) {
BookType.audio -> startActivityForResult<AudioPlayActivity>(
requestCodeRead
) {
putExtra("bookUrl", book.bookUrl)
putExtra("inBookshelf", viewModel.inBookshelf)
}
else -> startActivityForResult<ReadBookActivity>(
requestCodeRead
) {
putExtra("bookUrl", book.bookUrl)
putExtra("inBookshelf", viewModel.inBookshelf)
putExtra("key", IntentDataHelp.putData(book))
}
BookType.audio -> readBookResult.launch(
Intent(this, AudioPlayActivity::class.java)
.putExtra("bookUrl", book.bookUrl)
.putExtra("inBookshelf", viewModel.inBookshelf)
)
else -> readBookResult.launch(
Intent(this, ReadBookActivity::class.java)
.putExtra("bookUrl", book.bookUrl)
.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
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
@ -22,7 +22,12 @@ class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack {
private val resultSelectCover = 132
private val selectCoverResult =
registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { uri ->
coverChangeTo(uri)
}
}
override val viewModel: BookInfoEditViewModel
by viewModels()
@ -60,7 +65,7 @@ class BookInfoEditActivity :
}
}
tvSelectCover.setOnClickListener {
selectImage()
selectCoverResult.launch("image/*")
}
tvRefreshCover.setOnClickListener {
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) {
viewModel.book?.customCoverUrl = 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
import android.app.Activity
import android.content.Intent
import android.net.Uri
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.PermissionsCompat
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
@ -36,17 +34,31 @@ import java.util.*
* 导入本地书籍界面
*/
class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookViewModel>(),
FilePickerDialog.CallBack,
PopupMenu.OnMenuItemClickListener,
SelectActionBar.CallBack,
ImportBookAdapter.CallBack {
private val requestCodeSelectFolder = 342
ImportBookAdapter.CallBack,
SelectActionBar.CallBack {
private var rootDoc: DocumentFile? = null
private val subDocs = arrayListOf<DocumentFile>()
private lateinit var adapter: ImportBookAdapter
private var localUriLiveData: LiveData<List<String>>? = null
private var sdPath = FileUtils.getSdCardPath()
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
by viewModels()
@ -69,7 +81,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
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()
}
return super.onCompatOptionsItemSelected(item)
@ -129,14 +141,14 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
when {
lastPath.isNullOrEmpty() -> {
binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder)
selectFolder.launch(null)
}
lastPath.isContentScheme() -> {
val rootUri = Uri.parse(lastPath)
rootDoc = DocumentFile.fromTreeUri(this, rootUri)
if (rootDoc == null) {
binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder)
selectFolder.launch(null)
} else {
subDocs.clear()
upPath()
@ -144,7 +156,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
}
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
binding.tvEmptyMsg.visible()
FilePicker.selectFolder(this, requestCodeSelectFolder)
selectFolder.launch(null)
}
else -> {
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
override fun nextDoc(uri: Uri) {
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.searchContent.SearchContentActivity
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.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity
@ -71,23 +71,26 @@ class ReadBookActivity : ReadBookBaseActivity(),
ColorPickerDialogListener {
private val tocActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it.data?.let { data ->
data.getIntExtra("index", ReadBook.durChapterIndex).let { index ->
if (index != ReadBook.durChapterIndex) {
val chapterPos = data.getIntExtra("chapterPos", 0)
viewModel.openChapter(index, chapterPos)
}
registerForActivityResult(TocActivityResult()) {
it?.let {
if (it.first != ReadBook.durChapterIndex) {
viewModel.openChapter(it.first, it.second)
}
}
}
private val sourceEditActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
upView()
if (it.resultCode == RESULT_OK) {
viewModel.upBookSource {
upView()
}
}
}
private val replaceActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
viewModel.replaceRuleChanged()
if (it.resultCode == RESULT_OK) {
viewModel.replaceRuleChanged()
}
}
private val searchContentActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -727,9 +730,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
*/
override fun openChapterList() {
ReadBook.book?.let {
tocActivity.launch(Intent(this, ChapterListActivity::class.java).apply {
putExtra("bookUrl", it.bookUrl)
})
tocActivity.launch(it.bookUrl)
}
}

@ -1,13 +1,12 @@
package io.legado.app.ui.book.read.config
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog
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.getSecondaryTextColor
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import rxhttp.wrapper.param.RxHttp
import rxhttp.wrapper.param.toByteArray
import java.io.File
class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
class BgTextConfigDialog : BaseDialogFragment() {
companion object {
const val TEXT_COLOR = 121
@ -40,13 +39,27 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
}
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 lateinit var adapter: BgAdapter
private var primaryTextColor = 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() {
super.onStart()
@ -108,7 +121,9 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
tvName.text = getString(R.string.select_image)
ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor)
root.setOnClickListener { selectImage() }
root.setOnClickListener {
selectBgImage.launch("image/*")
}
}
}
requireContext().assets.list("bg")?.let {
@ -159,25 +174,20 @@ class BgTextConfigDialog : BaseDialogFragment(), FilePickerDialog.CallBack {
.show(requireActivity())
}
binding.ivImport.setOnClickListener {
val importFormNet = "网络导入"
val otherActions = arrayListOf(importFormNet)
FilePicker.selectFile(
this@BgTextConfigDialog,
requestCodeImport,
title = getString(R.string.import_str),
allowExtensions = arrayOf("zip"),
otherActions = otherActions
) { action ->
when (action) {
importFormNet -> importNetConfigAlert()
}
}
selectImportDoc.launch(
FilePickerParam(
mode = FilePicker.FILE,
title = getString(R.string.import_str),
allowExtensions = arrayOf("zip"),
otherActions = arrayOf(importFormNet)
)
)
}
binding.ivExport.setOnClickListener {
FilePicker.selectFolder(
this@BgTextConfigDialog,
requestCodeExport,
title = getString(R.string.export_str)
selectExportDir.launch(
FilePickerParam(
title = getString(R.string.export_str)
)
)
}
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")
private fun exportConfig(uri: Uri) {
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) {
if (uri.toString().isContentScheme()) {
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.data.appDb
import io.legado.app.data.entities.HttpTTS
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogHttpTtsEditBinding
import io.legado.app.databinding.DialogRecyclerViewBinding
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.primaryColor
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.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -33,10 +36,21 @@ import splitties.init.appCtx
class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
private val ttsUrlKey = "ttsUrlKey"
lateinit var adapter: Adapter
private val viewModel: SpeakEngineViewModel by viewModels()
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() {
super.onStart()
@ -100,10 +114,45 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
when (item?.itemId) {
R.id.menu_add -> editHttpTTS()
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
}
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")
private fun editHttpTTS(v: HttpTTS? = null) {
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
import android.app.Application
import android.net.Uri
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS
import io.legado.app.help.DefaultData
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.*
import rxhttp.wrapper.param.RxHttp
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 {
RxHttp.get(url).toText("utf-8").await().let { json ->
GSON.fromJsonArray<HttpTTS>(json)?.let {
appDb.httpTTSDao.insert(*it.toTypedArray())
}
import(json)
}
}.onSuccess {
finally("导入成功")
toastOnUi("导入成功")
}.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.lifecycle.LiveData
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import io.legado.app.R
@ -72,7 +71,6 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initView() = with(binding) {
adapter = TocRegexAdapter(requireContext())
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
recyclerView.adapter = adapter
val itemTouchCallback = ItemTouchCallback(adapter)
@ -137,7 +135,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
if (!cacheUrls.contains(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)
alertBinding.apply {
editView.setFilterValues(cacheUrls)

@ -1,6 +1,5 @@
package io.legado.app.ui.book.source.debug
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -12,10 +11,8 @@ import io.legado.app.databinding.ActivitySourceDebugBinding
import io.legado.app.help.LocalConfig
import io.legado.app.lib.theme.ATH
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.utils.startActivityForResult
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.launch
@ -26,7 +23,11 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
private lateinit var adapter: BookSourceDebugAdapter
private lateinit var searchView: SearchView
private val qrRequestCode = 101
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it?.let {
startSearch(it)
}
}
override fun getViewBinding(): ActivitySourceDebugBinding {
return ActivitySourceDebugBinding.inflate(layoutInflater)
@ -96,7 +97,7 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_scan -> {
startActivityForResult<QrCodeActivity>(qrRequestCode)
qrCodeResult.launch(null)
}
R.id.menu_help -> showHelp()
}
@ -108,16 +109,4 @@ class BookSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, BookS
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
import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
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.backgroundColor
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
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.dialog.TextDialog
import io.legado.app.utils.*
@ -36,13 +35,10 @@ import kotlin.math.abs
class BookSourceEditActivity :
VMBaseActivity<ActivityBookSourceEditBinding, BookSourceEditViewModel>(false),
FilePickerDialog.CallBack,
KeyboardToolPop.CallBack {
override val viewModel: BookSourceEditViewModel
by viewModels()
private val qrRequestCode = 101
private val selectPathRequestCode = 102
private val adapter = BookSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val searchEntities: ArrayList<EditEntity> = ArrayList()
@ -50,6 +46,20 @@ class BookSourceEditActivity :
private val infoEntities: ArrayList<EditEntity> = ArrayList()
private val tocEntities: 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 mIsSoftKeyBoardShowing = false
@ -98,7 +108,7 @@ class BookSourceEditActivity :
}
R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
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_qr -> shareWithQr(
GSON.toJson(getSource()),
@ -397,7 +407,11 @@ class BookSourceEditActivity :
0 -> insertText(AppConst.urlOption)
1 -> showRuleHelp()
2 -> showRegexHelp()
3 -> FilePicker.selectFile(this, selectPathRequestCode)
3 -> selectDoc.launch(
FilePickerParam(
mode = FilePicker.FILE
)
)
}
}
}
@ -425,28 +439,6 @@ class BookSourceEditActivity :
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 {
override fun onGlobalLayout() {
val rect = Rect()

@ -1,7 +1,6 @@
package io.legado.app.ui.book.source.manage
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
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.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
@ -45,15 +44,11 @@ import java.io.File
class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceViewModel>(),
PopupMenu.OnMenuItemClickListener,
BookSourceAdapter.CallBack,
FilePickerDialog.CallBack,
SelectActionBar.CallBack,
SearchView.OnQueryTextListener {
override val viewModel: BookSourceViewModel
by viewModels()
private val importRecordKey = "bookSourceRecordKey"
private val qrRequestCode = 101
private val importRequestCode = 132
private val exportRequestCode = 65
private lateinit var adapter: BookSourceAdapter
private lateinit var searchView: SearchView
private var bookSourceLiveDate: LiveData<List<BookSource>>? = null
@ -62,6 +57,37 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private var sort = Sort.Default
private var sortAscending = true
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 {
return ActivityBookSourceBinding.inflate(layoutInflater)
@ -95,15 +121,19 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
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()) {
startActivity(Intent.createChooser(it, getString(R.string.share_selected_source)))
}
R.id.menu_group_manage ->
GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json"))
R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_import_local -> importDoc.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_sort_manual -> {
item.isChecked = true
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_add_group -> selectionAddToGroups()
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
}
@ -372,7 +402,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
.getAsString(importRecordKey)
?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_book_source_on_line) {
alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setFilterValues(cacheUrls)
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() {
if (searchView.query.isNullOrEmpty()) {
super.finish()

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

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

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

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

@ -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.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
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.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.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.utils.applyTint
import io.legado.app.utils.getPrefString
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers
import splitties.init.appCtx
class BackupConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener,
FilePickerDialog.CallBack {
SharedPreferences.OnSharedPreferenceChangeListener {
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?) {
addPreferencesFromResource(R.xml.pref_config_backup)
@ -53,7 +127,7 @@ class BackupConfigFragment : BasePreferenceFragment(),
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
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?) {
@ -135,11 +209,11 @@ class BackupConfigFragment : BasePreferenceFragment(),
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
PreferKey.backupPath -> BackupRestoreUi.selectBackupFolder(this)
PreferKey.backupPath -> selectBackupPath.launch(null)
PreferKey.restoreIgnore -> restoreIgnore()
"web_dav_backup" -> BackupRestoreUi.backup(this)
"web_dav_restore" -> BackupRestoreUi.restore(this)
"import_old" -> BackupRestoreUi.importOldData(this)
"web_dav_backup" -> backup()
"web_dav_restore" -> restore()
"import_old" -> restoreOld.launch(null)
}
return super.onPreferenceTreeClick(preference)
}
@ -159,8 +233,81 @@ class BackupConfigFragment : BasePreferenceFragment(),
}.show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
BackupRestoreUi.onActivityResult(requestCode, resultCode, data)
fun backup() {
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
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.ComponentName
import android.content.Intent
import android.content.SharedPreferences
@ -10,6 +9,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.ListPreference
import androidx.preference.Preference
@ -38,14 +38,16 @@ import java.io.File
class OtherConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeCover = 231
private val packageManager = appCtx.packageManager
private val componentName = ComponentName(
appCtx,
SharedReceiverActivity::class.java.name
)
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?) {
putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
@ -88,13 +90,13 @@ class OtherConfigFragment : BasePreferenceFragment(),
}
PreferKey.cleanCache -> clearCache()
PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectImage(requestCodeCover)
selectCoverImage.launch("image/*")
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.defaultCover)
} else {
selectImage(requestCodeCover)
selectCoverImage.launch("image/*")
}
}
}
@ -183,14 +185,6 @@ class OtherConfigFragment : BasePreferenceFragment(),
}.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 {
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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
@ -11,6 +9,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import io.legado.app.R
@ -38,8 +37,18 @@ import java.io.File
class ThemeConfigFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val requestCodeBgImage = 234
private val requestCodeBgImageN = 342
private val selectLightBg = registerForActivityResult(ActivityResultContracts.GetContent()) {
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?) {
addPreferencesFromResource(R.xml.pref_config_theme)
@ -186,26 +195,26 @@ class ThemeConfigFragment : BasePreferenceFragment(),
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme", "saveNightTheme" -> saveThemeAlert(key)
PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) {
selectImage(requestCodeBgImage)
selectLightBg.launch("image/*")
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImage)
upTheme(false)
} else {
selectImage(requestCodeBgImage)
selectLightBg.launch("image/*")
}
}
}
PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) {
selectImage(requestCodeBgImageN)
selectDarkBg.launch("image/*")
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImageN)
upTheme(true)
} else {
selectImage(requestCodeBgImageN)
selectDarkBg.launch("image/*")
}
}
}
@ -213,13 +222,6 @@ class ThemeConfigFragment : BasePreferenceFragment(),
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")
private fun saveThemeAlert(key: String) {
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.net.Uri
import android.os.Bundle
@ -8,7 +8,6 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
@ -16,10 +15,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.databinding.DialogFileChooserBinding
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.adapter.FileAdapter
import io.legado.app.ui.filepicker.adapter.PathAdapter
import io.legado.app.ui.document.FilePicker.Companion.DIRECTORY
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.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -33,12 +33,9 @@ class FilePickerDialog : DialogFragment(),
companion object {
const val tag = "FileChooserDialog"
const val DIRECTORY = 0
const val FILE = 1
fun show(
manager: FragmentManager,
requestCode: Int,
mode: Int = FILE,
title: String? = null,
initPath: String? = null,
@ -51,7 +48,6 @@ class FilePickerDialog : DialogFragment(),
FilePickerDialog().apply {
val bundle = Bundle()
bundle.putInt("mode", mode)
bundle.putInt("requestCode", requestCode)
bundle.putString("title", title)
bundle.putBoolean("isShowHomeDir", isShowHomeDir)
bundle.putBoolean("isShowUpDir", isShowUpDir)
@ -71,20 +67,6 @@ class FilePickerDialog : DialogFragment(),
override var isShowHomeDir: Boolean = false
override var isShowUpDir: Boolean = true
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
private var initPath = FileUtils.getSdCardPath()
private var mode: Int = FILE
@ -111,7 +93,6 @@ class FilePickerDialog : DialogFragment(),
binding.toolBar.setBackgroundColor(primaryColor)
view.setBackgroundResource(R.color.background_card)
arguments?.let {
requestCode = it.getInt("requestCode")
mode = it.getInt("mode", FILE)
title = it.getString("title")
isShowHomeDir = it.getBoolean("isShowHomeDir")
@ -132,7 +113,7 @@ class FilePickerDialog : DialogFragment(),
}
initMenu()
initContentView()
queryPermission.launch(Permissions.Group.STORAGE)
refreshCurrentDirPath(initPath)
}
private fun initMenu() {
@ -168,11 +149,6 @@ class FilePickerDialog : DialogFragment(),
setData(it)
dismissAllowingStateLoss()
}
else -> item?.title?.let {
(parentFragment as? CallBack)?.onMenuClick(it.toString())
(activity as? CallBack)?.onMenuClick(it.toString())
dismissAllowingStateLoss()
}
}
return true
}
@ -225,14 +201,16 @@ class FilePickerDialog : DialogFragment(),
private fun setData(path: String) {
val data = Intent().setData(Uri.fromFile(File(path)))
(parentFragment as? CallBack)
?.onActivityResult(requestCode, Activity.RESULT_OK, data)
(activity as? CallBack)
?.onActivityResult(requestCode, Activity.RESULT_OK, data)
(parentFragment as? CallBack)?.onResult(data)
(activity as? CallBack)?.onResult(data)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
activity?.finish()
}
interface CallBack {
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
fun onMenuClick(menu: String) {}
fun onResult(data: Intent)
}
}

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.adapter
package io.legado.app.ui.document.adapter
import android.content.Context
@ -9,9 +9,9 @@ import io.legado.app.databinding.ItemFileFilepickerBinding
import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.getPrimaryDisabledTextColor
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.ui.filepicker.entity.FileItem
import io.legado.app.ui.filepicker.utils.ConvertUtils
import io.legado.app.ui.filepicker.utils.FilePickerIcon
import io.legado.app.ui.document.entity.FileItem
import io.legado.app.ui.document.utils.ConvertUtils
import io.legado.app.ui.document.utils.FilePickerIcon
import io.legado.app.utils.FileUtils
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.os.Environment
@ -6,8 +6,8 @@ import android.view.ViewGroup
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemPathFilepickerBinding
import io.legado.app.ui.filepicker.utils.ConvertUtils
import io.legado.app.ui.filepicker.utils.FilePickerIcon
import io.legado.app.ui.document.utils.ConvertUtils
import io.legado.app.ui.document.utils.FilePickerIcon
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

@ -1,4 +1,4 @@
package io.legado.app.ui.filepicker.entity
package io.legado.app.ui.document.entity
import java.io.Serializable
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.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

@ -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
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.view.Menu
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.local.ImportBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.books.BooksFragment
import io.legado.app.utils.*
@ -41,10 +39,8 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
*/
class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_bookshelf),
TabLayout.OnTabSelectedListener,
FilePickerDialog.CallBack,
SearchView.OnQueryTextListener {
private val requestCodeImportBookshelf = 312
private val binding by viewBinding(FragmentBookshelfBinding::bind)
override val viewModel: BookshelfViewModel by viewModels()
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 val bookGroups = mutableListOf<BookGroup>()
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?) {
tabLayout = binding.titleBar.findViewById(R.id.tab_layout)
@ -229,28 +230,16 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
}
noButton()
neutralButton(R.string.select_file) {
FilePicker.selectFile(
this@BookshelfFragment,
requestCodeImportBookshelf,
allowExtensions = arrayOf("txt", "json")
importBookshelf.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
}
}.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) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

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

@ -1,6 +1,5 @@
package io.legado.app.ui.main.my
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
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.ReadRecordActivity
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.ConfigViewModel
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.widget.dialog.TextDialog
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.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)
@ -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.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
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.service.help.ReadBook
import io.legado.app.ui.association.ImportReplaceRuleActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.TextDialog
@ -47,24 +46,52 @@ import java.io.File
class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRuleViewModel>(),
SearchView.OnQueryTextListener,
PopupMenu.OnMenuItemClickListener,
FilePickerDialog.CallBack,
SelectActionBar.CallBack,
ReplaceRuleAdapter.CallBack {
override val viewModel: ReplaceRuleViewModel by viewModels()
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 searchView: SearchView
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
private var replaceRuleLiveData: LiveData<List<ReplaceRule>>? = null
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 {
return ActivityReplaceRuleBinding.inflate(layoutInflater)
@ -189,10 +216,14 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
GroupManageDialog().show(supportFragmentManager, "groupManage")
R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection())
R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json"))
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(importRequestCodeQr)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_local -> importDoc.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_qr -> qrCodeResult.launch(null)
R.id.menu_help -> showHelp()
else -> if (item.groupId == R.id.replace_group) {
searchView.setQuery("group:${item.title}", true)
@ -205,7 +236,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
when (item?.itemId) {
R.id.menu_enable_selection -> viewModel.enableSelection(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
}
@ -263,47 +294,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
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() {
super.onDestroy()
Coroutine.async { ReadBook.contentProcessor?.upReplaceRules() }

@ -1,38 +1,47 @@
package io.legado.app.ui.rss.article
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityRssArtivlesBinding
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.utils.gone
import io.legado.app.utils.startActivityForResult
import io.legado.app.utils.visible
class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewModel>() {
override val viewModel: RssSortViewModel
by viewModels()
private val editSource = 12319
private val fragments = linkedMapOf<String, RssArticlesFragment>()
private lateinit var adapter: TabFragmentPageAdapter
private val upSourceResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK) {
viewModel.initData(intent) {
upFragments()
}
}
}
override fun getViewBinding(): ActivityRssArtivlesBinding {
return ActivityRssArtivlesBinding.inflate(layoutInflater)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
adapter = TabFragmentPageAdapter(supportFragmentManager)
binding.tabLayout.setupWithViewPager(binding.viewPager)
adapter = TabFragmentPageAdapter()
binding.viewPager.adapter = adapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = fragments.keys.elementAt(position)
}.attach()
viewModel.titleLiveData.observe(this, {
binding.titleBar.title = it
})
@ -49,9 +58,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let {
startActivityForResult<RssSourceEditActivity>(editSource) {
putExtra("data", it)
}
upSourceResult.launch(
Intent(this, RssSourceEditActivity::class.java)
.putExtra("data", it)
)
}
R.id.menu_clear -> {
viewModel.url?.let {
@ -79,35 +89,15 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
adapter.notifyDataSetChanged()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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
}
private inner class TabFragmentPageAdapter : FragmentStateAdapter(this) {
override fun getPageTitle(position: Int): CharSequence {
return fragments.keys.elementAt(position)
override fun getItemCount(): Int {
return fragments.size
}
override fun getItem(position: Int): Fragment {
override fun createFragment(position: Int): Fragment {
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.ImportReplaceRuleActivity
import io.legado.app.ui.association.ImportRssSourceActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.*
import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
@ -33,7 +33,6 @@ import splitties.systemservices.downloadManager
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(false),
FilePickerDialog.CallBack,
ReadRssViewModel.CallBack {
override val viewModel: ReadRssViewModel
@ -44,6 +43,10 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
private var ttsMenuItem: MenuItem? = null
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = 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 {
return ActivityRssReadBinding.inflate(layoutInflater)
@ -156,14 +159,11 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
if (!path.isNullOrEmpty()) {
default.add(path)
}
FilePicker.selectFolder(
this,
savePathRequestCode,
getString(R.string.save_image),
default
) {
viewModel.saveImage(webPic, it)
}
saveImage.launch(
FilePickerParam(
otherActions = default.toTypedArray()
)
)
}
@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() {
super.onDestroy()
binding.webView.destroy()

@ -1,7 +1,6 @@
package io.legado.app.ui.rss.source.edit
import android.app.Activity
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
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.selector
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.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog
@ -34,9 +33,15 @@ class RssSourceEditActivity :
private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false
private val qrRequestCode = 101
private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
}
}
override fun getViewBinding(): ActivityRssSourceEditBinding {
return ActivityRssSourceEditBinding.inflate(layoutInflater)
@ -107,7 +112,7 @@ class RssSourceEditActivity :
}
}
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_share_str -> share(GSON.toJson(getRssSource()))
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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
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.primaryTextColor
import io.legado.app.ui.association.ImportRssSourceActivity
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.widget.SelectActionBar
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 java.io.File
/**
* 订阅源管理
*/
class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceViewModel>(),
PopupMenu.OnMenuItemClickListener,
FilePickerDialog.CallBack,
SelectActionBar.CallBack,
RssSourceAdapter.CallBack {
override val viewModel: RssSourceViewModel
by viewModels()
private val importRecordKey = "rssSourceRecordKey"
private val qrRequestCode = 101
private val importRequestCode = 124
private val exportRequestCode = 65
private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>()
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 {
return ActivityRssSourceBinding.inflate(layoutInflater)
@ -81,10 +108,14 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add -> startActivity<RssSourceEditActivity>()
R.id.menu_import_source_local -> FilePicker
.selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json"))
R.id.menu_import_source_onLine -> showImportDialog()
R.id.menu_import_source_qr -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_import_local -> importDoc.launch(
FilePickerParam(
mode = FilePicker.FILE,
allowExtensions = arrayOf("txt", "json")
)
)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_qr -> qrCodeResult.launch(null)
R.id.menu_group_manage -> GroupManageDialog()
.show(supportFragmentManager, "rssGroupManage")
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_disable_selection -> viewModel.disableSelection(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_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray())
}
@ -235,7 +266,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
.getAsString(importRecordKey)
?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_book_source_on_line) {
alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setFilterValues(cacheUrls)
editView.delCallBack = {
@ -260,46 +291,6 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
}.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) {
viewModel.del(source)
}

@ -1,6 +1,5 @@
package io.legado.app.ui.widget.font
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
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.PermissionsCompat
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filepicker.FilePicker
import io.legado.app.ui.filepicker.FilePickerDialog
import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.Main
@ -32,16 +31,37 @@ import java.util.*
import kotlin.collections.ArrayList
class FontSelectDialog : BaseDialogFragment(),
FilePickerDialog.CallBack,
Toolbar.OnMenuItemClickListener,
FontAdapter.CallBack {
private val fontFolderRequestCode = 35485
private val fontRegex = Regex(".*\\.[ot]tf")
private val fontFolder by lazy {
FileUtils.createFolderIfNotExist(appCtx.filesDir, "Fonts")
}
private var adapter: FontAdapter? = null
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() {
super.onStart()
@ -108,19 +128,11 @@ class FontSelectDialog : BaseDialogFragment(),
private fun openFolder() {
launch(Main) {
val defaultPath = "SD${File.separator}Fonts"
FilePicker.selectFolder(
this@FontSelectDialog,
fontFolderRequestCode,
otherActions = arrayListOf(defaultPath)
) {
when (it) {
defaultPath -> {
val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts"
putPrefString(PreferKey.fontFolder, path)
loadFontFilesByPermission(path)
}
}
}
selectFontDir.launch(
FilePickerParam(
otherActions = arrayOf(defaultPath)
)
)
}
}
@ -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() {
callBack?.selectFont("")
}

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

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

@ -1,20 +1,11 @@
package io.legado.app.utils
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowInsets
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 {
val displayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

@ -3,7 +3,7 @@ package io.legado.app.utils
import android.os.Environment
import android.webkit.MimeTypeMap
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 java.io.*
import java.nio.charset.Charset

@ -10,10 +10,6 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.edit
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) =
requireContext().defaultSharedPreferences.getBoolean(key, defValue)
@ -63,11 +59,4 @@ inline fun <reified T : Activity> Fragment.startActivity(
configIntent: Intent.() -> Unit = {}
) {
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.net.Uri
import androidx.documentfile.provider.DocumentFile
import java.io.File
fun Uri.isContentScheme() = this.scheme == "content"
@Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray? {
if (this.toString().isContentScheme()) {
if (this.isContentScheme()) {
return DocumentUtils.readBytes(context, this)
} else {
val path = RealPathUtil.getPath(context, this)
@ -32,7 +33,7 @@ fun Uri.writeBytes(
context: Context,
byteArray: ByteArray
): Boolean {
if (this.toString().isContentScheme()) {
if (this.isContentScheme()) {
return DocumentUtils.writeBytes(context, byteArray, this)
} else {
val path = RealPathUtil.getPath(context, this)
@ -48,3 +49,22 @@ fun Uri.writeBytes(
fun Uri.writeText(context: Context, text: String): Boolean {
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:contentLayout="@layout/view_tab_layout" />
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

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

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

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

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

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

@ -15,4 +15,19 @@
android:title="@string/import_default_rule"
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>

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

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

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

@ -191,8 +191,8 @@
<string name="flow_sys">Follow system</string>
<string name="add">Add</string>
<string name="import_book_source">Import book sources</string>
<string name="import_book_source_local">Import local sources</string>
<string name="import_book_source_on_line">Import online sources</string>
<string name="import_local">Import local</string>
<string name="import_on_line">Import online</string>
<string name="replace_rule_title">Replacement</string>
<string name="replace_rule_edit">Edit replacement rule</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.
buildscript {
ext.kotlin_version = '1.4.31'
ext.kotlin_version = '1.4.32'
repositories {
google()
maven { url "https://maven.aliyun.com/nexus/content/groups/public/" }

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

@ -1,15 +1,15 @@
package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.util.ArrayList;
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.
*
* @author paul.siegmann
*
*/
public class NavigationHistory implements NavigationEventListener {
@ -58,7 +58,7 @@ public class NavigationHistory implements NavigationEventListener {
return currentSize;
}
public void initBook(Book book) {
public void initBook(EpubBook book) {
if (book == null) {
return;
}

@ -1,14 +1,15 @@
package me.ag2s.epublib.browsersupport;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Resource;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Resource;
/**
* A helper class for epub browser applications.
*
* <p>
* 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
* by calling the NavigationEventListeners.
@ -18,7 +19,7 @@ import java.util.List;
public class Navigator implements Serializable {
private static final long serialVersionUID = 1076126986424925474L;
private Book book;
private EpubBook book;
private int currentSpinePos;
private Resource currentResource;
private int currentPagePos;
@ -30,7 +31,7 @@ public class Navigator implements Serializable {
this(null);
}
public Navigator(Book book) {
public Navigator(EpubBook book) {
this.book = book;
this.currentSpinePos = 0;
if (book != null) {
@ -158,7 +159,7 @@ public class Navigator implements Serializable {
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);
this.book = book;
this.currentFragmentId = null;
@ -193,7 +194,7 @@ public class Navigator implements Serializable {
this.currentResource = book.getSpine().getResource(currentIndex);
}
public Book getBook() {
public EpubBook getBook() {
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;
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 extends Book {
public class EpubBook 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,6 +1,6 @@
package me.ag2s.epublib.epub;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.EpubBook;
/**
* Post-processes a book.
@ -16,5 +16,5 @@ public interface BookProcessor {
*/
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 me.ag2s.epublib.domain.Book;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import me.ag2s.epublib.domain.EpubBook;
/**
* A book processor that combines several other bookprocessors
*
* <p>
* Fixes coverpage/coverimage.
* Cleans up the XHTML.
*
@ -30,7 +31,7 @@ public class BookProcessorPipeline implements BookProcessor {
}
@Override
public Book processBook(Book book) {
public EpubBook processBook(EpubBook book) {
if (bookProcessors == null) {
return book;
}
@ -38,7 +39,7 @@ public class BookProcessorPipeline implements BookProcessor {
try {
book = bookProcessor.processBook(book);
} catch (Exception e) {
Log.e(TAG,e.getMessage(), e);
Log.e(TAG, e.getMessage(), e);
}
}
return book;

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

@ -2,10 +2,8 @@ package me.ag2s.epublib.epub;
import android.util.Log;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.MediaTypes;
import me.ag2s.epublib.domain.Resource;
import me.ag2s.epublib.util.IOUtil;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -14,7 +12,11 @@ import java.io.Writer;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
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.
@ -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);
ZipOutputStream resultStream = new ZipOutputStream(out);
writeMimeType(resultStream);
@ -51,19 +53,19 @@ public class EpubWriter {
resultStream.close();
}
private Book processBook(Book book) {
private EpubBook processBook(EpubBook book) {
if (bookProcessor != null) {
book = bookProcessor.processBook(book);
}
return book;
}
private void initTOCResource(Book book) {
private void initTOCResource(EpubBook book) {
Resource tocResource;
try {
if(book.isEpub3()){
if (book.isEpub3()) {
tocResource = NCXDocumentV3.createNCXResource(book);
}else {
} else {
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()) {
writeResource(resource, resultStream);
}
@ -108,11 +110,11 @@ public class EpubWriter {
}
private void writePackageDocument(Book book, ZipOutputStream resultStream)
throws IOException {
private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)
throws IOException {
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
XmlSerializer xmlSerializer = EpubProcessorSupport
.createXmlSerializer(resultStream);
.createXmlSerializer(resultStream);
PackageDocumentWriter.write(this, xmlSerializer, book);
xmlSerializer.flush();
// String resultAsString = result.toString();

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

@ -17,7 +17,7 @@ import java.util.List;
import me.ag2s.epublib.Constants;
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.MediaType;
import me.ag2s.epublib.domain.MediaTypes;
@ -96,7 +96,7 @@ public class NCXDocumentV3 {
* @return Resource
*/
@SuppressWarnings("unused")
public static Resource read(Book book, EpubReader epubReader) {
public static Resource read(EpubBook book, EpubReader epubReader) {
Resource ncxResource = null;
if (book.getSpine().getTocResource() == null) {
Log.e(TAG, "Book does not contain a table of contents file");
@ -126,7 +126,7 @@ public class NCXDocumentV3 {
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<>();
if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
@ -143,7 +143,7 @@ public class NCXDocumentV3 {
static List<TOCReference> readTOCReferences(NodeList navpoints,
Book book) {
EpubBook book) {
if (navpoints == null) {
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);
//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 {
return createNCXResource(book.getMetadata().getIdentifiers(),
book.getTitle(), book.getMetadata().getAuthors(),
@ -289,7 +289,7 @@ public class NCXDocumentV3 {
* @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException
*/
public static void write(XmlSerializer xmlSerializer, Book book)
public static void write(XmlSerializer xmlSerializer, EpubBook book)
throws IllegalArgumentException, IllegalStateException, IOException {
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
book.getMetadata().getAuthors(), book.getTableOfContents());

@ -1,39 +1,42 @@
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.domain.Author;
import me.ag2s.epublib.domain.Book;
import me.ag2s.epublib.domain.Date;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Identifier;
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 {
/**
* Writes the book's metadata.
*
* @param book book
* @param book book
* @param serializer serializer
* @throws IOException IOException
* @throws IllegalStateException IllegalStateException
* @throws IOException IOException
* @throws IllegalStateException IllegalStateException
* @throws IllegalArgumentException IllegalArgumentException
*/
public static void writeMetaData(Book book, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
public static void writeMetaData(EpubBook book, XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
writeIdentifiers(book.getMetadata().getIdentifiers(), serializer);
writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(),
serializer);
serializer);
writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(),
serializer);
serializer);
writeSimpleMetdataElements(DCTags.description,
book.getMetadata().getDescriptions(), serializer);
writeSimpleMetdataElements(DCTags.publisher,

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

@ -2,8 +2,15 @@ package me.ag2s.epublib.epub;
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.domain.Book;
import me.ag2s.epublib.domain.EpubBook;
import me.ag2s.epublib.domain.Guide;
import me.ag2s.epublib.domain.GuideReference;
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.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
*
@ -29,7 +29,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
private static final String TAG = PackageDocumentWriter.class.getName();
public static void write(EpubWriter epubWriter, XmlSerializer serializer,
Book book) {
EpubBook book) {
try {
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
@ -66,7 +66,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
* @throws IllegalArgumentException 1@throws XMLStreamException
*/
@SuppressWarnings("unused")
private static void writeSpine(Book book, EpubWriter epubWriter,
private static void writeSpine(EpubBook book, EpubWriter epubWriter,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
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)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);
@ -102,7 +102,7 @@ public class PackageDocumentWriter extends PackageDocumentBase {
//For EPUB3
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.href, NCXDocumentV3.DEFAULT_NCX_HREF);
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);
}
private static List<Resource> getAllResourcesSortById(Book book) {
private static List<Resource> getAllResourcesSortById(EpubBook book) {
List<Resource> allResources = new ArrayList<>(
book.getResources().getAll());
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
*
* @param resource g
* @param resource g
* @param serializer g
* @throws IOException g
* @throws IllegalStateException g
* @throws IOException g
* @throws IllegalStateException g
* @throws IllegalArgumentException 1@throws XMLStreamException
*/
private static void writeItem(Book book, Resource resource,
private static void writeItem(EpubBook book, Resource resource,
XmlSerializer serializer)
throws IllegalArgumentException, IllegalStateException, IOException {
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)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(NAMESPACE_OPF, OPFTags.guide);

Loading…
Cancel
Save