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.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>()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
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.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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue