Share logic for saving page/cover (#6787)
* Use MediaStore on newer Android Q or newer * Use flow instead of Observable * Review comment fixes * Use suspended function instead of flow
This commit is contained in:
parent
ddb856edc7
commit
1163aa4e4e
10 changed files with 265 additions and 143 deletions
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
addSingletonFactory { DelayedTrackingStore(app) }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
|
@ -6,8 +6,6 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that manages [PendingIntent] of activity's
|
* Class that manages [PendingIntent] of activity's
|
||||||
|
@ -32,9 +30,8 @@ object NotificationHandler {
|
||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param file file containing image
|
* @param file file containing image
|
||||||
*/
|
*/
|
||||||
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
val uri = file.getUriCompat(context)
|
|
||||||
setDataAndType(uri, "image/*")
|
setDataAndType(uri, "image/*")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
}
|
}
|
||||||
|
|
143
app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
Normal file
143
app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package eu.kanade.tachiyomi.data.saver
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import okio.IOException
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class ImageSaver(
|
||||||
|
val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
suspend fun save(image: Image): Uri {
|
||||||
|
val data = image.data
|
||||||
|
|
||||||
|
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
|
||||||
|
val filename = DiskUtil.buildValidFilename("${image.name}.$type")
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return save(data(), image.location.directory(context), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.location !is Location.Pictures) {
|
||||||
|
return save(data(), image.location.directory(context), filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pictureDir =
|
||||||
|
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||||
|
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.DISPLAY_NAME, image.name)
|
||||||
|
put(
|
||||||
|
MediaStore.Images.Media.RELATIVE_PATH,
|
||||||
|
"${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/" +
|
||||||
|
(image.location as Location.Pictures).relativePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val picture = context.contentResolver.insert(
|
||||||
|
pictureDir,
|
||||||
|
contentValues
|
||||||
|
) ?: throw IOException("Couldn't create file")
|
||||||
|
|
||||||
|
data().use { input ->
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
context.contentResolver.openOutputStream(picture, "w").use { output ->
|
||||||
|
input.copyTo(output!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return picture
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||||
|
directory.mkdirs()
|
||||||
|
|
||||||
|
val destFile = File(directory, filename)
|
||||||
|
|
||||||
|
inputStream.use { input ->
|
||||||
|
destFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destFile.getUriCompat(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Image(
|
||||||
|
open val name: String,
|
||||||
|
open val location: Location
|
||||||
|
) {
|
||||||
|
data class Cover(
|
||||||
|
val bitmap: Bitmap,
|
||||||
|
override val name: String,
|
||||||
|
override val location: Location
|
||||||
|
) : Image(name, location)
|
||||||
|
|
||||||
|
data class Page(
|
||||||
|
val inputStream: () -> InputStream,
|
||||||
|
override val name: String,
|
||||||
|
override val location: Location
|
||||||
|
) : Image(name, location)
|
||||||
|
|
||||||
|
val data: () -> InputStream
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
is Cover -> {
|
||||||
|
{
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
|
||||||
|
ByteArrayInputStream(baos.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Page -> inputStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Location {
|
||||||
|
data class Pictures private constructor(val relativePath: String) : Location() {
|
||||||
|
companion object {
|
||||||
|
fun create(relativePath: String = ""): Pictures {
|
||||||
|
return Pictures(relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Cache : Location()
|
||||||
|
|
||||||
|
fun directory(context: Context): File {
|
||||||
|
return when (this) {
|
||||||
|
Cache -> context.cacheImageDir
|
||||||
|
is Pictures -> {
|
||||||
|
val file = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||||
|
context.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
if (relativePath.isNotEmpty()) {
|
||||||
|
return File(
|
||||||
|
file,
|
||||||
|
relativePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.Image
|
||||||
|
import eu.kanade.tachiyomi.data.saver.Location
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
@ -85,7 +87,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.hasCustomCover
|
import eu.kanade.tachiyomi.util.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
@ -775,26 +777,47 @@ class MangaController :
|
||||||
|
|
||||||
fun shareCover() {
|
fun shareCover() {
|
||||||
try {
|
try {
|
||||||
|
val manga = manga!!
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
useCoverAsBitmap(activity) { coverBitmap ->
|
useCoverAsBitmap(activity) { coverBitmap ->
|
||||||
val cover = presenter.shareCover(activity, coverBitmap)
|
viewScope.launchIO {
|
||||||
val uri = cover.getUriCompat(activity)
|
val uri = presenter.saveImage(
|
||||||
startActivity(uri.toShareIntent(activity))
|
image = Image.Cover(
|
||||||
|
bitmap = coverBitmap,
|
||||||
|
name = manga.title,
|
||||||
|
location = Location.Cache
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launchUI {
|
||||||
|
startActivity(uri.toShareIntent(activity))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
activity?.toast(R.string.error_sharing_cover)
|
activity?.toast(R.string.error_saving_cover)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCover() {
|
fun saveCover() {
|
||||||
try {
|
try {
|
||||||
|
val manga = manga!!
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
useCoverAsBitmap(activity) { coverBitmap ->
|
useCoverAsBitmap(activity) { coverBitmap ->
|
||||||
presenter.saveCover(activity, coverBitmap)
|
viewScope.launchIO {
|
||||||
activity.toast(R.string.cover_saved)
|
presenter.saveImage(
|
||||||
|
image = Image.Cover(
|
||||||
|
bitmap = coverBitmap,
|
||||||
|
name = manga.title,
|
||||||
|
location = Location.Pictures.create()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launchUI {
|
||||||
|
activity.toast(R.string.cover_saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
activity?.toast(R.string.error_saving_cover)
|
activity?.toast(R.string.error_saving_cover)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.Image
|
||||||
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
@ -39,10 +41,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
import eu.kanade.tachiyomi.util.storage.getPicturesDir
|
|
||||||
import eu.kanade.tachiyomi.util.storage.getTempShareDir
|
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
|
@ -58,7 +56,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class MangaPresenter(
|
class MangaPresenter(
|
||||||
|
@ -110,6 +108,8 @@ class MangaPresenter(
|
||||||
|
|
||||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
|
private val imageSaver: ImageSaver by injectLazy()
|
||||||
|
|
||||||
private var trackSubscription: Subscription? = null
|
private var trackSubscription: Subscription? = null
|
||||||
private var searchTrackerJob: Job? = null
|
private var searchTrackerJob: Job? = null
|
||||||
private var refreshTrackersJob: Job? = null
|
private var refreshTrackersJob: Job? = null
|
||||||
|
@ -338,44 +338,13 @@ class MangaPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save manga cover Bitmap to temporary share directory.
|
* Save manga cover Bitmap to picture or temporary share directory.
|
||||||
*
|
*
|
||||||
* @param context for the temporary share directory
|
* @param image the image with specified location
|
||||||
* @param coverBitmap the cover to save (as Bitmap)
|
* @return flow Flow which emits the Uri which specifies where the image is saved when
|
||||||
* @return cover File in temporary share directory
|
|
||||||
*/
|
*/
|
||||||
fun shareCover(context: Context, coverBitmap: Bitmap): File {
|
suspend fun saveImage(image: Image): Uri {
|
||||||
return saveCover(getTempShareDir(context), coverBitmap)
|
return imageSaver.save(image)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save manga cover to pictures directory of the device.
|
|
||||||
*
|
|
||||||
* @param context for the pictures directory of the user
|
|
||||||
* @param coverBitmap the cover to save (as Bitmap)
|
|
||||||
* @return cover File in pictures directory
|
|
||||||
*/
|
|
||||||
fun saveCover(context: Context, coverBitmap: Bitmap) {
|
|
||||||
saveCover(getPicturesDir(context), coverBitmap)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a manga cover Bitmap to a new File in a given directory.
|
|
||||||
* Overwrites file if it already exists.
|
|
||||||
*
|
|
||||||
* @param directory The directory in which the new file will be created
|
|
||||||
* @param coverBitmap The manga cover to save
|
|
||||||
* @return the newly created File
|
|
||||||
*/
|
|
||||||
private fun saveCover(directory: File, coverBitmap: Bitmap): File {
|
|
||||||
directory.mkdirs()
|
|
||||||
val filename = DiskUtil.buildValidFilename("${manga.title}.${ImageUtil.ImageType.PNG}")
|
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
|
||||||
destFile.outputStream().use { desFileOutputStream ->
|
|
||||||
coverBitmap.compress(Bitmap.CompressFormat.PNG, 100, desFileOutputStream)
|
|
||||||
}
|
|
||||||
return destFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
@ -13,6 +12,7 @@ import android.graphics.ColorMatrix
|
||||||
import android.graphics.ColorMatrixColorFilter
|
import android.graphics.ColorMatrixColorFilter
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.drawable.RippleDrawable
|
import android.graphics.drawable.RippleDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
@ -69,13 +69,13 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.util.preference.toggle
|
import eu.kanade.tachiyomi.util.preference.toggle
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|
||||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||||
import eu.kanade.tachiyomi.util.system.getThemeColor
|
import eu.kanade.tachiyomi.util.system.getThemeColor
|
||||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.copy
|
import eu.kanade.tachiyomi.util.view.copy
|
||||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||||
|
@ -89,7 +89,6 @@ import kotlinx.coroutines.flow.sample
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@ -830,18 +829,14 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||||
* Called from the presenter when a page is ready to be shared. It shows Android's default
|
* Called from the presenter when a page is ready to be shared. It shows Android's default
|
||||||
* sharing tool.
|
* sharing tool.
|
||||||
*/
|
*/
|
||||||
fun onShareImageResult(file: File, page: ReaderPage) {
|
fun onShareImageResult(uri: Uri, page: ReaderPage) {
|
||||||
val manga = presenter.manga ?: return
|
val manga = presenter.manga ?: return
|
||||||
val chapter = page.chapter.chapter
|
val chapter = page.chapter.chapter
|
||||||
|
|
||||||
val uri = file.getUriCompat(this)
|
val intent = uri.toShareIntent(
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
context = applicationContext,
|
||||||
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number))
|
message = getString(R.string.share_page_info, manga.title, chapter.name, page.number)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
)
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
type = "image/*"
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader
|
package eu.kanade.tachiyomi.ui.reader
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -10,6 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.saver.Image
|
||||||
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
|
import eu.kanade.tachiyomi.data.saver.Location
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingUpdateJob
|
||||||
|
@ -28,11 +32,10 @@ import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.byteSize
|
import eu.kanade.tachiyomi.util.lang.byteSize
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.lang.takeBytes
|
import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getPicturesDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import eu.kanade.tachiyomi.util.storage.getTempShareDir
|
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
|
@ -45,7 +48,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -92,6 +95,8 @@ class ReaderPresenter(
|
||||||
*/
|
*/
|
||||||
private val isLoadingAdjacentChapterRelay = BehaviorRelay.create<Boolean>()
|
private val isLoadingAdjacentChapterRelay = BehaviorRelay.create<Boolean>()
|
||||||
|
|
||||||
|
private val imageSaver: ImageSaver by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
||||||
* time in a background thread to avoid blocking the UI.
|
* time in a background thread to avoid blocking the UI.
|
||||||
|
@ -560,32 +565,6 @@ class ReaderPresenter(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the image of this [page] in the given [directory] and returns the file location.
|
|
||||||
*/
|
|
||||||
private fun saveImage(page: ReaderPage, directory: File, manga: Manga): File {
|
|
||||||
val stream = page.stream!!
|
|
||||||
val type = ImageUtil.findImageType(stream) ?: throw Exception("Not an image")
|
|
||||||
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
val chapter = page.chapter.chapter
|
|
||||||
|
|
||||||
// Build destination file.
|
|
||||||
val filenameSuffix = " - ${page.number}.${type.extension}"
|
|
||||||
val filename = DiskUtil.buildValidFilename(
|
|
||||||
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize())
|
|
||||||
) + filenameSuffix
|
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
|
||||||
stream().use { input ->
|
|
||||||
destFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return destFile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the image of this [page] on the pictures directory and notifies the UI of the result.
|
* 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.
|
* There's also a notification to allow sharing the image somewhere else or deleting it.
|
||||||
|
@ -593,32 +572,42 @@ class ReaderPresenter(
|
||||||
fun saveImage(page: ReaderPage) {
|
fun saveImage(page: ReaderPage) {
|
||||||
if (page.status != Page.READY) return
|
if (page.status != Page.READY) return
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
val context = Injekt.get<Application>()
|
|
||||||
|
|
||||||
|
val context = Injekt.get<Application>()
|
||||||
val notifier = SaveImageNotifier(context)
|
val notifier = SaveImageNotifier(context)
|
||||||
notifier.onClear()
|
notifier.onClear()
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
val chapter = page.chapter.chapter
|
||||||
|
val filenameSuffix = " - ${page.number}"
|
||||||
|
val filename = DiskUtil.buildValidFilename(
|
||||||
|
"${manga.title} - ${chapter.name}".takeBytes(MAX_FILE_NAME_BYTES - filenameSuffix.byteSize())
|
||||||
|
) + filenameSuffix
|
||||||
|
|
||||||
// Pictures directory.
|
// Pictures directory.
|
||||||
val baseDir = getPicturesDir(context).absolutePath
|
val relativePath = if (preferences.folderPerManga()) DiskUtil.buildValidFilename(manga.title) else ""
|
||||||
val destDir = if (preferences.folderPerManga()) {
|
|
||||||
File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
|
|
||||||
} else {
|
|
||||||
File(baseDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
|
||||||
.doOnNext { file ->
|
try {
|
||||||
DiskUtil.scanMedia(context, file)
|
presenterScope.launchIO {
|
||||||
notifier.onComplete(file)
|
val uri = imageSaver.save(
|
||||||
|
image = Image.Page(
|
||||||
|
inputStream = page.stream!!,
|
||||||
|
name = filename,
|
||||||
|
location = Location.Pictures.create(relativePath)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
launchUI {
|
||||||
|
DiskUtil.scanMedia(context, uri)
|
||||||
|
notifier.onComplete(uri)
|
||||||
|
view!!.onSaveImageResult(SaveImageResult.Success(uri))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.doOnError { notifier.onError(it.message) }
|
} catch (e: Throwable) {
|
||||||
.subscribeOn(Schedulers.io())
|
notifier.onError(e.message)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
view!!.onSaveImageResult(SaveImageResult.Error(e))
|
||||||
.subscribeFirst(
|
}
|
||||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
|
||||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -631,18 +620,27 @@ class ReaderPresenter(
|
||||||
fun shareImage(page: ReaderPage) {
|
fun shareImage(page: ReaderPage) {
|
||||||
if (page.status != Page.READY) return
|
if (page.status != Page.READY) return
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
|
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
|
val destDir = context.cacheImageDir
|
||||||
|
|
||||||
val destDir = getTempShareDir(context)
|
try {
|
||||||
|
presenterScope.launchIO {
|
||||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
destDir.deleteRecursively()
|
||||||
.map { saveImage(page, destDir, manga) }
|
val uri = imageSaver.save(
|
||||||
.subscribeOn(Schedulers.io())
|
image = Image.Page(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
inputStream = page.stream!!,
|
||||||
.subscribeFirst(
|
name = manga.title,
|
||||||
{ view, file -> view.onShareImageResult(file, page) },
|
location = Location.Cache
|
||||||
{ _, _ -> /* Empty */ }
|
)
|
||||||
)
|
)
|
||||||
|
launchUI {
|
||||||
|
view!!.onShareImageResult(uri, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -691,7 +689,7 @@ class ReaderPresenter(
|
||||||
* Results of the save image feature.
|
* Results of the save image feature.
|
||||||
*/
|
*/
|
||||||
sealed class SaveImageResult {
|
sealed class SaveImageResult {
|
||||||
class Success(val file: File) : SaveImageResult()
|
class Success(val uri: Uri) : SaveImageResult()
|
||||||
class Error(val error: Throwable) : SaveImageResult()
|
class Error(val error: Throwable) : SaveImageResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
|
@ -13,7 +14,6 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to show BigPictureStyle notifications
|
* Class used to show BigPictureStyle notifications
|
||||||
|
@ -36,14 +36,14 @@ class SaveImageNotifier(private val context: Context) {
|
||||||
*
|
*
|
||||||
* @param file image file containing downloaded page image.
|
* @param file image file containing downloaded page image.
|
||||||
*/
|
*/
|
||||||
fun onComplete(file: File) {
|
fun onComplete(uri: Uri) {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = ImageRequest.Builder(context)
|
||||||
.data(file)
|
.data(uri)
|
||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.size(720, 1280)
|
.size(720, 1280)
|
||||||
.target(
|
.target(
|
||||||
onSuccess = { result ->
|
onSuccess = { result ->
|
||||||
showCompleteNotification(file, (result as BitmapDrawable).bitmap)
|
showCompleteNotification(uri, (result as BitmapDrawable).bitmap)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
onError(null)
|
onError(null)
|
||||||
|
@ -53,7 +53,7 @@ class SaveImageNotifier(private val context: Context) {
|
||||||
context.imageLoader.enqueue(request)
|
context.imageLoader.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCompleteNotification(file: File, image: Bitmap) {
|
private fun showCompleteNotification(uri: Uri, image: Bitmap) {
|
||||||
with(notificationBuilder) {
|
with(notificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.picture_saved))
|
setContentTitle(context.getString(R.string.picture_saved))
|
||||||
setSmallIcon(R.drawable.ic_photo_24dp)
|
setSmallIcon(R.drawable.ic_photo_24dp)
|
||||||
|
@ -64,18 +64,18 @@ class SaveImageNotifier(private val context: Context) {
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
clearActions()
|
clearActions()
|
||||||
|
|
||||||
setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
|
setContentIntent(NotificationHandler.openImagePendingActivity(context, uri))
|
||||||
// Share action
|
// Share action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId)
|
NotificationReceiver.shareImagePendingBroadcast(context, uri.path!!, notificationId)
|
||||||
)
|
)
|
||||||
// Delete action
|
// Delete action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_delete_24dp,
|
R.drawable.ic_delete_24dp,
|
||||||
context.getString(R.string.action_delete),
|
context.getString(R.string.action_delete),
|
||||||
NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId)
|
NotificationReceiver.deleteImagePendingBroadcast(context, uri.path!!, notificationId)
|
||||||
)
|
)
|
||||||
|
|
||||||
updateNotification()
|
updateNotification()
|
||||||
|
|
|
@ -3,20 +3,13 @@ package eu.kanade.tachiyomi.util.storage
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun getTempShareDir(context: Context) = File(context.cacheDir, "shared_image")
|
val Context.cacheImageDir: File
|
||||||
|
get() = File(cacheDir, "shared_image")
|
||||||
fun getPicturesDir(context: Context) = File(
|
|
||||||
Environment.getExternalStorageDirectory().absolutePath +
|
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
|
||||||
File.separator + context.getString(R.string.app_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the uri of a file
|
* Returns the uri of a file
|
||||||
|
|
|
@ -6,10 +6,11 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
fun Uri.toShareIntent(context: Context, type: String = "image/*"): Intent {
|
fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent {
|
||||||
val uri = this
|
val uri = this
|
||||||
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
if (message != null) putExtra(Intent.EXTRA_TEXT, message)
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
clipData = ClipData.newRawUri(null, uri)
|
||||||
setType(type)
|
setType(type)
|
||||||
|
|
Reference in a new issue