Option to save/share combined double pages

This commit is contained in:
Jays2Kings 2021-04-12 14:49:43 -04:00
parent 443887c89a
commit 2ec4db3c10
9 changed files with 221 additions and 40 deletions

View File

@ -12,6 +12,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.TextViewCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -52,9 +53,19 @@ open class MaterialMenuSheet(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.isInNightMode() && !activity.window.decorView.rootWindowInsets.hasSideNavBar()) {
window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
maxHeight?.let {
binding.menuScrollView.maxHeight = it + activity.window.decorView.rootWindowInsets.systemWindowInsetBottom
if (maxHeight != null) {
binding.menuScrollView.maxHeight = maxHeight + activity.window.decorView.rootWindowInsets.systemWindowInsetBottom
binding.menuScrollView.requestLayout()
} else {
binding.titleLayout.viewTreeObserver.addOnGlobalLayoutListener {
binding.menuScrollView.updateLayoutParams<ConstraintLayout.LayoutParams> {
val fullHeight = activity.window.decorView.height
val insets = activity.window.decorView.rootWindowInsets
matchConstraintMaxHeight =
fullHeight - (insets?.systemWindowInsetTop ?: 0) -
binding.titleLayout.height - 26.dpToPx
}
}
}
binding.divider.visibleIf(showDivider)

View File

@ -9,11 +9,14 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att
var maxHeight = -1
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightS = if (maxHeight > 0) {
var heightS = if (maxHeight > 0) {
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
} else {
heightMeasureSpec
}
if (maxHeight < height + (rootWindowInsets?.systemWindowInsetBottom ?: 0)) {
heightS = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
}
super.onMeasure(widthMeasureSpec, heightS)
}
}

View File

@ -968,6 +968,16 @@ class ReaderActivity :
2,
R.drawable.ic_photo_24dp,
R.string.set_first_page_as_cover
),
MaterialMenuSheet.MenuSheetItem(
6,
R.drawable.ic_share_all_outline_24dp,
R.string.share_combined_pages
),
MaterialMenuSheet.MenuSheetItem(
7,
R.drawable.ic_save_all_outline_24dp,
R.string.save_combined_pages
)
)
} else {
@ -997,6 +1007,22 @@ class ReaderActivity :
3 -> extraPage?.let { shareImage(it) }
4 -> extraPage?.let { saveImage(it) }
5 -> extraPage?.let { showSetCoverPrompt(it) }
6, 7 -> extraPage?.let { secondPage ->
(viewer as? PagerViewer)?.let { viewer ->
val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages)
val bg =
if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) {
Color.WHITE
} else {
Color.BLACK
}
if (item == 6) {
presenter.shareImages(page, secondPage, isLTR, bg)
} else {
presenter.saveImages(page, secondPage, isLTR, bg)
}
}
}
}
true
}.show()
@ -1051,17 +1077,22 @@ class ReaderActivity :
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
*/
fun onShareImageResult(file: File, page: ReaderPage) {
fun onShareImageResult(file: File, page: ReaderPage, secondPage: ReaderPage? = null) {
val manga = presenter.manga ?: return
val chapter = page.chapter.chapter
val decimalFormat =
DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
val pageNumber = if (secondPage != null) {
getString(R.string.pages_, if (resources.isLTR) "${page.number}-${page.number + 1}" else "${page.number + 1}-${page.number}")
} else {
getString(R.string.page_, page.number)
}
val text = "${manga.title}: ${getString(
R.string.chapter_,
decimalFormat.format(chapter.chapter_number)
)}, ${getString(R.string.page_, page.number)}"
)}, $pageNumber"
val stream = file.getUriCompat(this)
val intent = Intent(Intent.ACTION_SEND).apply {

View File

@ -1,9 +1,11 @@
package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import androidx.annotation.ColorInt
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -32,8 +34,11 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import rx.Completable
@ -117,6 +122,8 @@ class ReaderPresenter(
var chapterItems = emptyList<ReaderChapterItem>()
private var scope = CoroutineScope(Job() + Dispatchers.Default)
/**
* Called when the presenter is created. It retrieves the saved active chapter if the process
* was restored.
@ -610,6 +617,40 @@ class ReaderPresenter(
return destFile
}
/**
* Saves the image of this [page] in the given [directory] and returns the file location.
*/
private fun saveImages(page1: ReaderPage, page2: ReaderPage, isLTR: Boolean, @ColorInt bg: Int, directory: File, manga: Manga): File {
val stream1 = page1.stream!!
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
val stream2 = page2.stream!!
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
val imageBytes = stream1().readBytes()
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val imageBytes2 = stream2().readBytes()
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
val stream = ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg)
directory.mkdirs()
val chapter = page1.chapter.chapter
// Build destination file.
val filename = DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".take(225)
) + " - ${page1.number}-${page2.number}.jpg"
val destFile = File(directory, filename)
stream.use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
stream.close()
return destFile
}
/**
* Saves the image of this [page] on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
@ -644,6 +685,34 @@ class ReaderPresenter(
)
}
fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
scope.launch {
if (firstPage.status != Page.READY) return@launch
if (secondPage.status != Page.READY) return@launch
val manga = manga ?: return@launch
val context = Injekt.get<Application>()
val notifier = SaveImageNotifier(context)
notifier.onClear()
// Pictures directory.
val destDir = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + "Tachiyomi"
)
try {
val file = saveImages(firstPage, secondPage, isLTR, bg, destDir, manga)
DiskUtil.scanMedia(context, file)
notifier.onComplete(file)
withUIContext { view?.onSaveImageResult(SaveImageResult.Success(file)) }
} catch (e: Exception) {
withUIContext { view?.onSaveImageResult(SaveImageResult.Error(e)) }
}
}
}
/**
* Shares the image of this [page] and notifies the UI with the path of the file to share.
* The image must be first copied to the internal partition because there are many possible
@ -668,6 +737,25 @@ class ReaderPresenter(
)
}
fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) {
scope.launch {
if (firstPage.status != Page.READY) return@launch
if (secondPage.status != Page.READY) return@launch
val manga = manga ?: return@launch
val context = Injekt.get<Application>()
val destDir = File(context.cacheDir, "shared_image")
destDir.deleteRecursively()
try {
val file = saveImages(firstPage, secondPage, isLTR, bg, destDir, manga)
withUIContext {
view?.onShareImageResult(file, firstPage, secondPage)
}
} catch (e: Exception) {
}
}
}
/**
* Sets the image of this [page] as cover and notifies the UI of the result.
*/

View File

@ -3,12 +3,9 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.GestureDetector
import android.view.Gravity
@ -54,11 +51,8 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.roundToInt
/**
@ -683,36 +677,24 @@ class PagerPageHolder(
skipExtra = true
return imageBytes.inputStream()
}
val maxHeight = max(height, height2)
val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
canvas.drawColor(if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) Color.WHITE else Color.BLACK)
val isLTR = (viewer !is R2LPagerViewer).xor(viewer.config.invertDoublePages)
val upperPart = Rect(
if (isLTR) 0 else width2,
(maxHeight - imageBitmap.height) / 2,
(if (isLTR) 0 else width2) + imageBitmap.width,
imageBitmap.height + (maxHeight - imageBitmap.height) / 2
)
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
scope?.launchUI { progressBar.setProgress(98) }
val bottomPart = Rect(
if (!isLTR) 0 else width,
(maxHeight - imageBitmap2.height) / 2,
(if (!isLTR) 0 else width) + imageBitmap2.width,
imageBitmap2.height + (maxHeight - imageBitmap2.height) / 2
)
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
scope?.launchUI { progressBar.setProgress(99) }
val bg = if (viewer.config.readerTheme >= 2 || viewer.config.readerTheme == 0) {
Color.WHITE
} else {
Color.BLACK
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
imageStream.close()
imageStream2.close()
scope?.launchUI { progressBar.completeAndFadeOut() }
return ByteArrayInputStream(output.toByteArray())
return ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg) {
scope?.launchUI {
if (it == 100) {
progressBar.completeAndFadeOut()
} else {
progressBar.setProgress(it)
}
}
}
}
private fun splitDoublePages() {
@ -734,9 +716,6 @@ class PagerPageHolder(
}
}
private val Bitmap.rect: Rect
get() = Rect(0, 0, width, height)
companion object {
fun getBGType(readerTheme: Int, context: Context): Int {
return if (readerTheme == 3) {

View File

@ -3,14 +3,20 @@ package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import androidx.annotation.ColorInt
import eu.kanade.tachiyomi.R
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URLConnection
import kotlin.math.abs
import kotlin.math.max
object ImageUtil {
@ -244,6 +250,47 @@ object ImageUtil {
return ColorDrawable(backgroundColor)
}
fun mergeBitmaps(
imageBitmap: Bitmap,
imageBitmap2: Bitmap,
isLTR: Boolean,
@ColorInt background: Int = Color.WHITE,
progressCallback: ((Int) -> Unit)? = null
): ByteArrayInputStream {
val height = imageBitmap.height
val width = imageBitmap.width
val height2 = imageBitmap2.height
val width2 = imageBitmap2.width
val maxHeight = max(height, height2)
val result = Bitmap.createBitmap(width + width2, max(height, height2), Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
canvas.drawColor(background)
val upperPart = Rect(
if (isLTR) 0 else width2,
(maxHeight - imageBitmap.height) / 2,
(if (isLTR) 0 else width2) + imageBitmap.width,
imageBitmap.height + (maxHeight - imageBitmap.height) / 2
)
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
progressCallback?.invoke(98)
val bottomPart = Rect(
if (!isLTR) 0 else width,
(maxHeight - imageBitmap2.height) / 2,
(if (!isLTR) 0 else width) + imageBitmap2.width,
imageBitmap2.height + (maxHeight - imageBitmap2.height) / 2
)
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
progressCallback?.invoke(99)
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray())
}
private val Bitmap.rect: Rect
get() = Rect(0, 0, width, height)
fun Boolean.toInt() = if (this) 1 else 0
private fun isDark(color: Int): Boolean {
return Color.red(color) < 40 && Color.blue(color) < 40 && Color.green(color) < 40 &&

View File

@ -0,0 +1,9 @@
<!-- drawable/content_save_all_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path android:fillColor="#000" android:pathData="M1 7H3V21H17V23H3C1.9 23 1 22.11 1 21V7M19 1H7C5.89 1 5 1.9 5 3V17C5 18.1 5.89 19 7 19H21C22.1 19 23 18.1 23 17V5L19 1M21 17H7V3H18.17L21 5.83V17M14 10C12.34 10 11 11.34 11 13S12.34 16 14 16 17 14.66 17 13 15.66 10 14 10M8 4H17V8H8V4Z" />
</vector>

View File

@ -0,0 +1,9 @@
<!-- drawable/share_all_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/actionBarTintColor">
<path android:fillColor="#000" android:pathData="M13 9.8V10.7L11.3 10.9C8.7 11.3 6.8 12.3 5.4 13.6C7.1 13.1 8.9 12.8 11 12.8H13V14.1L15.2 12L13 9.8M11 5L18 12L11 19V14.9C6 14.9 2.5 16.5 0 20C1 15 4 10 11 9M17 8V5L24 12L17 19V16L21 12" />
</vector>

View File

@ -284,6 +284,7 @@
<string name="set_as_default_for_all">Set as default for all</string>
<string name="cover_updated">Cover updated</string>
<string name="page_">Page %1$d</string>
<string name="pages_">Pages %1$s</string>
<string name="next_chapter_not_found">Next chapter not found</string>
<string name="decode_image_error">The image could not be decoded</string>
<string name="use_image_as_cover">Use this image as cover art?</string>
@ -311,6 +312,9 @@
<string name="share_second_page">Share second page</string>
<string name="save_second_page">Save second page</string>
<string name="share_combined_pages">Share combined pages</string>
<string name="save_combined_pages">Save combined pages</string>
<!-- Reader settings -->
<string name="fullscreen">Fullscreen</string>
<string name="animate_page_transitions">Animate page transitions</string>