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:
Andreas 2022-03-19 21:46:23 +01:00 committed by GitHub
parent ddb856edc7
commit 1163aa4e4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 265 additions and 143 deletions

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
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.job.DelayedTrackingStore
import eu.kanade.tachiyomi.extension.ExtensionManager
@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
get<PreferencesHelper>()

View file

@ -6,8 +6,6 @@ import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
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
@ -32,9 +30,8 @@ object NotificationHandler {
* @param context context of application
* @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 uri = file.getUriCompat(context)
setDataAndType(uri, "image/*")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}

View 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
}
}
}
}

View 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.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)
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)
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)
}

View file

@ -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)
}
/**

View file

@ -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)))
}

View file

@ -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)
}
.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)) }
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))
}
}
} 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()
}

View file

@ -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()

View file

@ -3,20 +3,13 @@ package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import java.io.File
fun getTempShareDir(context: Context) = File(context.cacheDir, "shared_image")
fun getPicturesDir(context: Context) = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + context.getString(R.string.app_name)
)
val Context.cacheImageDir: File
get() = File(cacheDir, "shared_image")
/**
* Returns the uri of a file

View file

@ -6,10 +6,11 @@ import android.content.Intent
import android.net.Uri
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 shareIntent = Intent(Intent.ACTION_SEND).apply {
if (message != null) putExtra(Intent.EXTRA_TEXT, message)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
setType(type)