diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaterialMenuSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaterialMenuSheet.kt index ac11b91def..7b085dd2a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaterialMenuSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaterialMenuSheet.kt @@ -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 { + 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaxHeightScrollView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaxHeightScrollView.kt index bc8f5749d6..1d81ab6f67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaxHeightScrollView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/MaxHeightScrollView.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 50d763c85a..bc25481a2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index e9c868c1d0..ba67493be4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -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() + 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() + + 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() + + 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index f857ad3f1a..e624b4a041 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index 3ed26ad53a..18bc8d8ef0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -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 && diff --git a/app/src/main/res/drawable/ic_save_all_outline_24dp.xml b/app/src/main/res/drawable/ic_save_all_outline_24dp.xml new file mode 100644 index 0000000000..6345f96d26 --- /dev/null +++ b/app/src/main/res/drawable/ic_save_all_outline_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share_all_outline_24dp.xml b/app/src/main/res/drawable/ic_share_all_outline_24dp.xml new file mode 100644 index 0000000000..e668612eb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_all_outline_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26b872a772..2b19e0dbeb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,6 +284,7 @@ Set as default for all Cover updated Page %1$d + Pages %1$s Next chapter not found The image could not be decoded Use this image as cover art? @@ -311,6 +312,9 @@ Share second page Save second page + Share combined pages + Save combined pages + Fullscreen Animate page transitions