Save and share manga cover #253

master
Koitharu 2 years ago
parent 1e1e9fabdc
commit bf8838f943
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -7,11 +7,12 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader import coil.ImageLoader
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ErrorResult import coil.request.ErrorResult
@ -20,17 +21,26 @@ import coil.request.SuccessResult
import coil.target.ViewTarget import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.PopupMenuMediator
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ActivityImageBinding import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.databinding.ItemErrorStateBinding import org.koitharu.kotatsu.databinding.ItemErrorStateBinding
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listener, View.OnClickListener { class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listener, View.OnClickListener {
@ -39,27 +49,45 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var errorBinding: ItemErrorStateBinding? = null private var errorBinding: ItemErrorStateBinding? = null
private val viewModel: ImageViewModel by viewModels()
private lateinit var menuMediator: PopupMenuMediator
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityImageBinding.inflate(layoutInflater)) setContentView(ActivityImageBinding.inflate(layoutInflater))
viewBinding.buttonBack.setOnClickListener(this) viewBinding.buttonBack.setOnClickListener(this)
loadImage(intent.data) viewBinding.buttonMenu.setOnClickListener(this)
val imageUrl = requireNotNull(intent.data)
val menuProvider = ImageMenuProvider(
activity = this,
snackbarHost = viewBinding.root,
viewModel = viewModel,
)
menuMediator = PopupMenuMediator(menuProvider)
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.root, null))
viewModel.onImageSaved.observeEvent(this, ::onImageSaved)
loadImage(imageUrl)
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
with(viewBinding.buttonBack) { viewBinding.buttonBack.updateLayoutParams<ViewGroup.MarginLayoutParams> {
updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin = insets.top + bottomMargin
topMargin = insets.top + marginBottom leftMargin = insets.left + bottomMargin
leftMargin = insets.left + marginBottom rightMargin = insets.right + bottomMargin
rightMargin = insets.right + marginBottom
} }
viewBinding.buttonMenu.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top + bottomMargin
leftMargin = insets.left + bottomMargin
rightMargin = insets.right + bottomMargin
} }
} }
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_back -> dispatchNavigateUp() R.id.button_back -> dispatchNavigateUp()
R.id.button_menu -> menuMediator.onLongClick(v)
else -> loadImage(intent.data) else -> loadImage(intent.data)
} }
} }
@ -92,11 +120,34 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this) .lifecycle(this)
.listener(this) .listener(this)
.tag(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE)) .source(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
.target(SsivTarget(viewBinding.ssiv)) .target(SsivTarget(viewBinding.ssiv))
.enqueueWith(coil) .enqueueWith(coil)
} }
private fun onImageSaved(uri: Uri) {
Snackbar.make(viewBinding.root, R.string.page_saved, Snackbar.LENGTH_LONG)
.setAction(R.string.share) {
ShareHelper(this).shareImage(uri)
}.show()
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val button = viewBinding.buttonMenu
button.isClickable = !isLoading
if (isLoading) {
button.setImageDrawable(
CircularProgressDrawable(this).also {
it.setStyle(CircularProgressDrawable.LARGE)
it.setColorSchemeColors(getThemeColor(com.google.android.material.R.attr.colorControlNormal))
it.start()
},
)
} else {
button.setImageResource(materialR.drawable.abc_ic_menu_overflow_material)
}
}
private class SsivTarget( private class SsivTarget(
override val view: SubsamplingScaleImageView, override val view: SubsamplingScaleImageView,
) : ViewTarget<SubsamplingScaleImageView> { ) : ViewTarget<SubsamplingScaleImageView> {
@ -124,7 +175,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
companion object { companion object {
private const val EXTRA_SOURCE = "source" const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, url: String, source: MangaSource?): Intent { fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
return Intent(context, ImageActivity::class.java) return Intent(context, ImageActivity::class.java)

@ -0,0 +1,68 @@
package org.koitharu.kotatsu.image.ui
import android.Manifest
import android.os.Build
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuProvider
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.local.data.isZipUri
class ImageMenuProvider(
private val activity: ComponentActivity,
private val snackbarHost: View,
private val viewModel: ImageViewModel,
) : MenuProvider {
private val permissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (isGranted) {
saveImage()
}
}
private val saveLauncher = activity.registerForActivityResult(
ActivityResultContracts.CreateDocument("image/png"),
) { uri ->
if (uri != null) {
viewModel.saveImage(uri)
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_image, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_save -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
saveImage()
}
true
}
else -> false
}
private fun saveImage() {
val name = activity.intent.data?.let {
if (it.isZipUri()) {
it.fragment
} else {
it.lastPathSegment
}?.substringBeforeLast('.')?.plus(".png")
}
if (name == null || !saveLauncher.tryLaunch(name)) {
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
}

@ -0,0 +1,50 @@
package org.koitharu.kotatsu.image.ui
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.SavedStateHandle
import coil.ImageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.source
import javax.inject.Inject
@HiltViewModel
class ImageViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val savedStateHandle: SavedStateHandle,
private val coil: ImageLoader,
) : BaseViewModel() {
val onImageSaved = MutableEventFlow<Uri>()
fun saveImage(destination: Uri) {
launchLoadingJob(Dispatchers.Default) {
val request = ImageRequest.Builder(context)
.memoryCachePolicy(CachePolicy.READ_ONLY)
.data(savedStateHandle.require<Uri>(BaseActivity.EXTRA_DATA))
.memoryCachePolicy(CachePolicy.DISABLED)
.source(savedStateHandle[ImageActivity.EXTRA_SOURCE])
.build()
val bitmap = coil.execute(request).getDrawableOrThrow().toBitmap()
runInterruptible(Dispatchers.IO) {
context.contentResolver.openOutputStream(destination)?.use { output ->
check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, output))
} ?: error("Cannot open output stream")
}
onImageSaved.call(destination)
}
}
}

@ -24,6 +24,18 @@
android:scaleType="center" android:scaleType="center"
app:srcCompat="?homeAsUpIndicator" /> app:srcCompat="?homeAsUpIndicator" />
<ImageButton
android:id="@+id/button_menu"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_margin="@dimen/screen_padding"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/back"
android:elevation="@dimen/m3_sys_elevation_level1"
android:scaleType="center"
app:srcCompat="@drawable/abc_ic_menu_overflow_material" />
<com.google.android.material.progressindicator.CircularProgressIndicator <com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_save"
android:title="@string/save" />
</menu>

@ -4,7 +4,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.4.0' classpath 'com.android.tools.build:gradle:8.4.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20'

Loading…
Cancel
Save