mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-15 05:27:28 +01:00
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:
@@ -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.model.Download
|
||||
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.TrackService
|
||||
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.hasCustomCover
|
||||
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.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@@ -775,26 +777,47 @@ class MangaController :
|
||||
|
||||
fun shareCover() {
|
||||
try {
|
||||
val manga = manga!!
|
||||
val activity = activity!!
|
||||
useCoverAsBitmap(activity) { coverBitmap ->
|
||||
val cover = presenter.shareCover(activity, coverBitmap)
|
||||
val uri = cover.getUriCompat(activity)
|
||||
startActivity(uri.toShareIntent(activity))
|
||||
viewScope.launchIO {
|
||||
val uri = presenter.saveImage(
|
||||
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)
|
||||
activity?.toast(R.string.error_sharing_cover)
|
||||
activity?.toast(R.string.error_saving_cover)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCover() {
|
||||
try {
|
||||
val manga = manga!!
|
||||
val activity = activity!!
|
||||
useCoverAsBitmap(activity) { coverBitmap ->
|
||||
presenter.saveCover(activity, coverBitmap)
|
||||
activity.toast(R.string.cover_saved)
|
||||
viewScope.launchIO {
|
||||
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)
|
||||
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.model.Download
|
||||
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.TrackManager
|
||||
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.removeCovers
|
||||
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.toast
|
||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||
@@ -58,7 +56,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
class MangaPresenter(
|
||||
@@ -110,6 +108,8 @@ class MangaPresenter(
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
private val imageSaver: ImageSaver by injectLazy()
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
private var searchTrackerJob: 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 coverBitmap the cover to save (as Bitmap)
|
||||
* @return cover File in temporary share directory
|
||||
* @param image the image with specified location
|
||||
* @return flow Flow which emits the Uri which specifies where the image is saved when
|
||||
*/
|
||||
fun shareCover(context: Context, coverBitmap: Bitmap): File {
|
||||
return saveCover(getTempShareDir(context), coverBitmap)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
suspend fun saveImage(image: Image): Uri {
|
||||
return imageSaver.save(image)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
@@ -13,6 +12,7 @@ import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
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.pager.R2LPagerViewer
|
||||
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.createReaderThemeContext
|
||||
import eu.kanade.tachiyomi.util.system.getThemeColor
|
||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||
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.view.copy
|
||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||
@@ -89,7 +89,6 @@ import kotlinx.coroutines.flow.sample
|
||||
import logcat.LogPriority
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
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
|
||||
* sharing tool.
|
||||
*/
|
||||
fun onShareImageResult(file: File, page: ReaderPage) {
|
||||
fun onShareImageResult(uri: Uri, page: ReaderPage) {
|
||||
val manga = presenter.manga ?: return
|
||||
val chapter = page.chapter.chapter
|
||||
|
||||
val uri = file.getUriCompat(this)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, 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/*"
|
||||
}
|
||||
val intent = uri.toShareIntent(
|
||||
context = applicationContext,
|
||||
message = getString(R.string.share_page_info, manga.title, chapter.name, page.number)
|
||||
)
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
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.download.DownloadManager
|
||||
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.job.DelayedTrackingStore
|
||||
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.lang.byteSize
|
||||
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.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.storage.cacheImageDir
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||
@@ -45,7 +48,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -92,6 +95,8 @@ class ReaderPresenter(
|
||||
*/
|
||||
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
|
||||
* 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.
|
||||
* 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) {
|
||||
if (page.status != Page.READY) return
|
||||
val manga = manga ?: return
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
val context = Injekt.get<Application>()
|
||||
val notifier = SaveImageNotifier(context)
|
||||
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.
|
||||
val baseDir = getPicturesDir(context).absolutePath
|
||||
val destDir = if (preferences.folderPerManga()) {
|
||||
File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
|
||||
} else {
|
||||
File(baseDir)
|
||||
}
|
||||
val relativePath = if (preferences.folderPerManga()) DiskUtil.buildValidFilename(manga.title) else ""
|
||||
|
||||
// Copy file in background.
|
||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||
.doOnNext { file ->
|
||||
DiskUtil.scanMedia(context, file)
|
||||
notifier.onComplete(file)
|
||||
|
||||
try {
|
||||
presenterScope.launchIO {
|
||||
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) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onSaveImageResult(SaveImageResult.Success(file)) },
|
||||
{ view, error -> view.onSaveImageResult(SaveImageResult.Error(error)) }
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
notifier.onError(e.message)
|
||||
view!!.onSaveImageResult(SaveImageResult.Error(e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -631,18 +620,27 @@ class ReaderPresenter(
|
||||
fun shareImage(page: ReaderPage) {
|
||||
if (page.status != Page.READY) return
|
||||
val manga = manga ?: return
|
||||
|
||||
val context = Injekt.get<Application>()
|
||||
val destDir = context.cacheImageDir
|
||||
|
||||
val destDir = getTempShareDir(context)
|
||||
|
||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
||||
.map { saveImage(page, destDir, manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst(
|
||||
{ view, file -> view.onShareImageResult(file, page) },
|
||||
{ _, _ -> /* Empty */ }
|
||||
)
|
||||
try {
|
||||
presenterScope.launchIO {
|
||||
destDir.deleteRecursively()
|
||||
val uri = imageSaver.save(
|
||||
image = Image.Page(
|
||||
inputStream = page.stream!!,
|
||||
name = manga.title,
|
||||
location = Location.Cache
|
||||
)
|
||||
)
|
||||
launchUI {
|
||||
view!!.onShareImageResult(uri, page)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -691,7 +689,7 @@ class ReaderPresenter(
|
||||
* Results of the save image feature.
|
||||
*/
|
||||
sealed class SaveImageResult {
|
||||
class Success(val file: File) : SaveImageResult()
|
||||
class Success(val uri: Uri) : SaveImageResult()
|
||||
class Error(val error: Throwable) : SaveImageResult()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import coil.imageLoader
|
||||
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.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Class used to show BigPictureStyle notifications
|
||||
@@ -36,14 +36,14 @@ class SaveImageNotifier(private val context: Context) {
|
||||
*
|
||||
* @param file image file containing downloaded page image.
|
||||
*/
|
||||
fun onComplete(file: File) {
|
||||
fun onComplete(uri: Uri) {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(file)
|
||||
.data(uri)
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.size(720, 1280)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
showCompleteNotification(file, (result as BitmapDrawable).bitmap)
|
||||
showCompleteNotification(uri, (result as BitmapDrawable).bitmap)
|
||||
},
|
||||
onError = {
|
||||
onError(null)
|
||||
@@ -53,7 +53,7 @@ class SaveImageNotifier(private val context: Context) {
|
||||
context.imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
private fun showCompleteNotification(file: File, image: Bitmap) {
|
||||
private fun showCompleteNotification(uri: Uri, image: Bitmap) {
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.picture_saved))
|
||||
setSmallIcon(R.drawable.ic_photo_24dp)
|
||||
@@ -64,18 +64,18 @@ class SaveImageNotifier(private val context: Context) {
|
||||
// Clear old actions if they exist
|
||||
clearActions()
|
||||
|
||||
setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
|
||||
setContentIntent(NotificationHandler.openImagePendingActivity(context, uri))
|
||||
// Share action
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId)
|
||||
NotificationReceiver.shareImagePendingBroadcast(context, uri.path!!, notificationId)
|
||||
)
|
||||
// Delete action
|
||||
addAction(
|
||||
R.drawable.ic_delete_24dp,
|
||||
context.getString(R.string.action_delete),
|
||||
NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId)
|
||||
NotificationReceiver.deleteImagePendingBroadcast(context, uri.path!!, notificationId)
|
||||
)
|
||||
|
||||
updateNotification()
|
||||
|
||||
Reference in New Issue
Block a user