Add share and save cover actions (closes #3011)

(cherry picked from commit 281a3911f69b96050bae4b232af94ca3671b92ae)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
This commit is contained in:
arkon 2021-06-01 18:36:06 -04:00 committed by Jobobby04
parent 6d7e8cdcb9
commit 88102b9705
13 changed files with 132 additions and 235 deletions

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -130,16 +130,8 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
*/
private fun shareImage(context: Context, path: String, notificationId: Int) {
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = "image/*"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
dismissNotification(context, notificationId)
// Launch share activity
context.startActivity(intent)
context.startActivity(File(path).getUriCompat(context).toShareIntent())
}
/**
@ -150,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
*/
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = fileMimeType
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
dismissNotification(context, notificationId)
// Launch share activity
context.startActivity(sendIntent)
context.startActivity(uri.toShareIntent())
}
/**

View File

@ -9,7 +9,6 @@ import android.widget.ArrayAdapter
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.isVisible
import coil.loadAny
import coil.transform.RoundedCornersTransformation
import com.afollestad.materialdialogs.MaterialDialog
@ -27,7 +26,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import exh.util.dropBlank
import exh.util.trimOrNull
import kotlinx.coroutines.flow.launchIn
@ -76,10 +74,7 @@ class EditMangaDialog : DialogController {
}
fun onViewCreated() {
val radius = context.resources.getDimension(R.dimen.card_radius)
binding.mangaCover.loadAny(manga) {
transformations(RoundedCornersTransformation(radius))
}
loadCover()
val isLocal = manga.source == LocalSource.ID
@ -159,14 +154,6 @@ class EditMangaDialog : DialogController {
binding.resetTags.clicks()
.onEach { resetTags() }
.launchIn(infoController.viewScope)
binding.resetCover.isVisible = !isLocal
binding.resetCover.clicks()
.onEach {
context.toast(R.string.cover_reset_toast)
customCoverUri = null
willResetCover = true
}
.launchIn(infoController.viewScope)
}
private fun resetTags() {
@ -177,6 +164,13 @@ class EditMangaDialog : DialogController {
}
}
fun loadCover() {
val radius = context.resources.getDimension(R.dimen.card_radius)
binding.mangaCover.loadAny(manga) {
transformations(RoundedCornersTransformation(radius))
}
}
fun updateCover(uri: Uri) {
willResetCover = false
val radius = context.resources.getDimension(R.dimen.card_radius)

View File

@ -5,7 +5,6 @@ import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.app.Activity
import android.content.ClipData
import android.content.Intent
import android.graphics.Color
import android.graphics.Point
@ -99,6 +98,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
@ -123,7 +123,6 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.ArrayDeque
import kotlin.math.min
@ -239,8 +238,6 @@ class MangaController :
private var editMergedSettingsDialog: EditMergedSettingsDialog? = null
private var currentAnimator: Animator? = null
private var isExpanded: Boolean = false
// EXH <--
init {
@ -510,16 +507,6 @@ class MangaController :
menu.findItem(R.id.action_recommend).isVisible = preferences.recommendsInOverflow().get()
menu.findItem(R.id.action_merged).isVisible = presenter.manga.source == MERGED_SOURCE_ID
menu.findItem(R.id.action_toggle_dedupe).isVisible = false // presenter.manga.source == MERGED_SOURCE_ID
val shareCoverItem = menu.findItem(R.id.action_share_cover)
val saveCoverItem = menu.findItem(R.id.action_save)
if (isExpanded) {
menu.forEach {
it.isVisible = it == shareCoverItem || it == saveCoverItem
}
} else {
shareCoverItem.isVisible = false
saveCoverItem.isVisible = false
}
// SY <--
}
@ -530,6 +517,10 @@ class MangaController :
R.id.download_custom, R.id.download_unread, R.id.download_all
-> downloadChapters(item.itemId)
R.id.action_share_cover -> shareCover()
R.id.action_save_cover -> saveCover()
// SY --> R.id.action_edit_cover -> changeCover() // SY <--
// SY -->
R.id.action_edit -> {
editMangaDialog = EditMangaDialog(
@ -556,34 +547,7 @@ class MangaController :
// SY <--
R.id.action_edit_categories -> onCategoriesClick()
// SY --> R.id.action_edit_cover -> handleChangeCover() // SY <--
R.id.action_migrate -> migrateManga()
// SY -->
R.id.action_save -> {
try {
presenter.saveCover(activity!!)
activity?.toast(R.string.cover_saved)
} catch (e: Exception) {
e.message?.let { activity?.toast(it) } ?: activity?.toast(R.string.error_saving_cover)
}
}
R.id.action_share_cover -> {
try {
val activity = activity!!
val cover = presenter.shareCover(activity)
val stream = cover.getUriCompat(activity)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, stream)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
clipData = ClipData.newRawUri(null, stream)
type = "image/*"
}
startActivity(Intent.createChooser(intent, activity.getString(R.string.action_share)))
} catch (e: Exception) {
e.message?.let { activity?.toast(it) } ?: activity?.toast(R.string.error_sharing_cover)
}
}
// SY <--
}
return super.onOptionsItemSelected(item)
}
@ -769,22 +733,6 @@ class MangaController :
}
// SY -->
fun changeCover() {
if (manga?.favorite == true || source?.id == LocalSource.ID) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
startActivityForResult(
Intent.createChooser(
intent,
resources?.getString(R.string.action_edit_cover)
),
REQUEST_EDIT_MANGA_COVER
)
} else {
activity?.toast(R.string.notification_first_add_to_library)
}
}
fun setRefreshing() {
isRefreshingInfo = true
updateRefreshing()
@ -931,8 +879,6 @@ class MangaController :
binding.expandedImage.pivotX = 0f
binding.expandedImage.pivotY = 0f
isExpanded = true
activity?.invalidateOptionsMenu()
currentAnimator = AnimatorSet().apply {
play(
@ -965,8 +911,6 @@ class MangaController :
binding.expandedImage.clicks()
.onEach {
isExpanded = false
activity?.invalidateOptionsMenu()
currentAnimator?.cancel()
currentAnimator = AnimatorSet().apply {
@ -1051,20 +995,35 @@ class MangaController :
}
}
private fun handleChangeCover() {
val manga = manga ?: return
if (manga.hasCustomCover(coverCache)) {
showEditCoverDialog(manga)
} else {
openMangaCoverPicker(manga)
private fun shareCover() {
try {
val activity = activity!!
val cover = presenter.shareCover(activity)
val uri = cover.getUriCompat(activity)
startActivity(Intent.createChooser(uri.toShareIntent(), activity.getString(R.string.action_share)))
} catch (e: Exception) {
Timber.e(e)
activity?.toast(R.string.error_sharing_cover)
}
}
/**
* Edit custom cover for selected manga.
*/
private fun showEditCoverDialog(manga: Manga) {
ChangeMangaCoverDialog(this, manga).showDialog(router)
private fun saveCover() {
try {
presenter.saveCover(activity!!)
activity?.toast(R.string.cover_saved)
} catch (e: Exception) {
Timber.e(e)
activity?.toast(R.string.error_saving_cover)
}
}
fun changeCover() {
val manga = manga ?: return
if (manga.hasCustomCover(coverCache)) {
ChangeMangaCoverDialog(this, manga).showDialog(router)
} else {
openMangaCoverPicker(manga)
}
}
override fun openMangaCoverPicker(manga: Manga) {
@ -1097,27 +1056,16 @@ class MangaController :
val dataUri = data?.data
if (dataUri == null || resultCode != Activity.RESULT_OK) return
val activity = activity ?: return
presenter.editCover(manga!!, activity, dataUri)
// SY -->
if (editMangaDialog != null) {
editMangaDialog?.updateCover(dataUri)
} else presenter.editCover(manga!!, activity, dataUri)
// SY <--
}
// SY -->
if (requestCode == REQUEST_EDIT_MANGA_COVER) {
if (data == null || resultCode != Activity.RESULT_OK) return
val activity = activity ?: return
try {
val uri = data.data ?: return
if (editMangaDialog != null) editMangaDialog?.updateCover(uri)
else {
presenter.editCoverWithStream(activity, uri)
}
} catch (error: IOException) {
activity.toast(R.string.notification_cover_update_failed)
Timber.e(error)
}
}
// SY <--
}
fun onSetCoverSuccess() {
editMangaDialog?.loadCover()
mangaInfoAdapter?.notifyDataSetChanged()
activity?.toast(R.string.cover_updated)
}

View File

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -42,6 +40,8 @@ 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.toast
import eu.kanade.tachiyomi.util.updateCoverLastModified
@ -357,7 +357,7 @@ class MangaPresenter(
}
if (uri != null) {
editCoverWithStream(context, uri)
editCover(manga, context, uri)
} else if (resetCover) {
coverCache.deleteCustomCover(manga)
manga.updateCoverLastModified(db)
@ -385,25 +385,6 @@ class MangaPresenter(
}
}
fun editCoverWithStream(context: Context, uri: Uri): Boolean {
val inputStream = context.contentResolver.openInputStream(uri) ?: return false
if (manga.source == LocalSource.ID) {
val cover = LocalSource.updateCover(context, manga, inputStream)
if (manga.thumbnail_url.isNullOrBlank() && cover != null) {
manga.thumbnail_url = cover.absolutePath
db.updateMangaThumbnail(manga).executeAsBlocking()
}
return true
}
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, inputStream)
manga.updateCoverLastModified(db)
return true
}
return false
}
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
val originalManga = db.getManga(originalMangaId).executeAsBlocking() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
if (originalManga.source == MERGED_SOURCE_ID) {
@ -545,41 +526,6 @@ class MangaPresenter(
fun toggleDedupe() {
// I cant find any way to call the chapter list subscription to get the chapters again
}
fun shareCover(context: Context): File {
val destDir = File(context.cacheDir, "shared_image")
return saveCover(destDir)
}
fun saveCover(context: Context) {
val directory = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + context.getString(R.string.app_name)
)
saveCover(directory)
}
private fun saveCover(directory: File): File {
val cover = coverCache.getCoverFile(manga) ?: throw Exception("Cover url was null")
if (!cover.exists()) throw Exception("Cover not in cache")
val type = ImageUtil.findImageType(cover.inputStream())
?: throw Exception("Not an image")
directory.mkdirs()
// Build destination file.
val filename = DiskUtil.buildValidFilename("${manga.title}.${type.extension}")
val destFile = File(directory, filename)
cover.inputStream().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
return destFile
}
// SY <--
/**
@ -661,6 +607,33 @@ class MangaPresenter(
moveMangaToCategories(manga, listOfNotNull(category))
}
fun shareCover(context: Context): File {
return saveCover(getTempShareDir(context))
}
fun saveCover(context: Context) {
saveCover(getPicturesDir(context))
}
private fun saveCover(directory: File): File {
val cover = coverCache.getCoverFile(manga) ?: throw Exception("Cover url was null")
if (!cover.exists()) throw Exception("Cover not in cache")
val type = ImageUtil.findImageType(cover.inputStream())
?: throw Exception("Not an image")
directory.mkdirs()
val filename = DiskUtil.buildValidFilename("${manga.title}.${type.extension}")
val destFile = File(directory, filename)
cover.inputStream().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
return destFile
}
/**
* Update cover with local file.
*

View File

@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Bundle
import android.os.Environment
import androidx.annotation.ColorInt
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
@ -36,6 +35,8 @@ import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO
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.updateCoverLastModified
import exh.md.utils.FollowStatus
@ -704,9 +705,7 @@ class ReaderPresenter(
notifier.onClear()
// Pictures directory.
val baseDir = Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + context.getString(R.string.app_name)
val baseDir = getPicturesDir(context).absolutePath
val destDir = if (preferences.folderPerManga()) {
File(baseDir + File.separator + manga.title)
} else {
@ -739,11 +738,7 @@ class ReaderPresenter(
notifier.onClear()
// Pictures directory.
val destDir = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + context.getString(R.string.app_name)
)
val destDir = getPicturesDir(context)
// Copy file in background.
Observable.fromCallable { saveImages(firstPage, secondPage, isLTR, bg, destDir, manga) }
@ -804,7 +799,7 @@ class ReaderPresenter(
val manga = manga ?: return
val context = Injekt.get<Application>()
val destDir = File(context.cacheDir, "shared_image")
val destDir = getTempShareDir(context)
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
.map { saveImage(page, destDir, manga) }
@ -823,7 +818,7 @@ class ReaderPresenter(
val manga = manga ?: return
val context = Injekt.get<Application>()
val destDir = File(context.cacheDir, "shared_image")
val destDir = getTempShareDir(context)
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
.map { saveImages(firstPage, secondPage, isLTR, bg, destDir, manga) }

View File

@ -3,11 +3,21 @@ 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)
)
/**
* Returns the uri of a file
*

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.util.system
import android.content.ClipData
import android.content.Intent
import android.net.Uri
fun Uri.toShareIntent(): Intent {
val uri = this
return Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = "image/*"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
}

View File

@ -29,19 +29,9 @@
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/reset_cover"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Theme.Widget.Button.FilledAccent"
android:textAllCaps="false"
android:layout_gravity="center"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@string/reset_cover" />
<Spinner
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/status"
android:minHeight="48dp"
android:layout_width="wrap_content"
android:layout_height="match_parent" />

View File

@ -38,13 +38,25 @@
</item>
<item
android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories"
app:showAsAction="never" />
android:id="@+id/cover_group"
android:title="@string/manga_cover"
app:showAsAction="never">
<menu>
<item
android:id="@+id/action_share_cover"
android:title="@string/action_share" />
<item
android:id="@+id/action_save_cover"
android:title="@string/action_save" />
<item
android:id="@+id/action_edit_cover"
android:title="@string/action_edit" />
</menu>
</item>
<item
android:id="@+id/action_edit_cover"
android:title="@string/action_edit_cover"
android:id="@+id/action_edit_categories"
android:title="@string/action_edit_categories"
app:showAsAction="never" />
<item
@ -80,20 +92,4 @@
android:title="@string/toggle_dedupe"
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save_black_24dp"
android:title="@string/action_save"
android:visible="false"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_share_cover"
android:icon="@drawable/ic_share_24dp"
android:title="@string/share_cover"
android:visible="false"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
</menu>

View File

@ -255,10 +255,6 @@
<string name="az_recommends">Voir les recommandations</string>
<string name="merge">Fusionner</string>
<string name="merge_with_another_source">Fusionner avec une autre</string>
<string name="cover_saved">Couverture sauvegardée</string>
<string name="share_cover">Partage de la couverture</string>
<string name="error_saving_cover">Erreurs sauvegarde de la couverture</string>
<string name="error_sharing_cover">Erreurs du partage de la couverture</string>
<!-- Manga info fragment -->
<string name="hiatus">Hiatus</string>

View File

@ -304,10 +304,6 @@
<string name="az_recommends">Смотреть Рекомендации</string>
<string name="merge">Миграция</string>
<string name="merge_with_another_source">Слиться С Другим</string>
<string name="cover_saved">Обложка сохранена </string>
<string name="share_cover">Поделиться обложкой </string>
<string name="error_saving_cover">Ошибка при сохранении обложки</string>
<string name="error_sharing_cover">Ошибка при публикации обложки</string>
<!-- Manga info fragment -->
<string name="hiatus">Перерыв</string>

View File

@ -559,6 +559,10 @@
<string name="download_custom">Custom</string>
<string name="download_all">All</string>
<string name="download_unread">Unread</string>
<string name="manga_cover">Cover</string>
<string name="cover_saved">Cover saved</string>
<string name="error_saving_cover">Error saving cover</string>
<string name="error_sharing_cover">Error sharing cover</string>
<string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string>
<string name="invalid_download_dir">Invalid download location</string>
<string name="chapter_settings">Chapter settings</string>

View File

@ -315,10 +315,6 @@
<string name="az_recommends">See Recommendations</string>
<string name="merge">Merge</string>
<string name="merge_with_another_source">Merge With Another</string>
<string name="cover_saved">Cover saved</string>
<string name="share_cover">Share cover</string>
<string name="error_saving_cover">Error saving cover</string>
<string name="error_sharing_cover">Error sharing cover</string>
<!-- Manga info fragment -->
<string name="hiatus">Hiatus</string>