Save and share manga cover #253
This commit is contained in:
@@ -7,11 +7,12 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ErrorResult
|
||||
@@ -20,17 +21,26 @@ import coil.request.SuccessResult
|
||||
import coil.target.ViewTarget
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.util.PopupMenuMediator
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
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.ItemErrorStateBinding
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listener, View.OnClickListener {
|
||||
@@ -39,27 +49,45 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
private var errorBinding: ItemErrorStateBinding? = null
|
||||
private val viewModel: ImageViewModel by viewModels()
|
||||
private lateinit var menuMediator: PopupMenuMediator
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityImageBinding.inflate(layoutInflater))
|
||||
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) {
|
||||
with(viewBinding.buttonBack) {
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top + marginBottom
|
||||
leftMargin = insets.left + marginBottom
|
||||
rightMargin = insets.right + marginBottom
|
||||
}
|
||||
viewBinding.buttonBack.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top + bottomMargin
|
||||
leftMargin = insets.left + bottomMargin
|
||||
rightMargin = insets.right + bottomMargin
|
||||
}
|
||||
viewBinding.buttonMenu.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top + bottomMargin
|
||||
leftMargin = insets.left + bottomMargin
|
||||
rightMargin = insets.right + bottomMargin
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_back -> dispatchNavigateUp()
|
||||
R.id.button_menu -> menuMediator.onLongClick(v)
|
||||
else -> loadImage(intent.data)
|
||||
}
|
||||
}
|
||||
@@ -92,11 +120,34 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
|
||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||
.lifecycle(this)
|
||||
.listener(this)
|
||||
.tag(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
|
||||
.source(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
|
||||
.target(SsivTarget(viewBinding.ssiv))
|
||||
.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(
|
||||
override val view: SubsamplingScaleImageView,
|
||||
) : ViewTarget<SubsamplingScaleImageView> {
|
||||
@@ -124,7 +175,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
const val EXTRA_SOURCE = "source"
|
||||
|
||||
fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
|
||||
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"
|
||||
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
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
9
app/src/main/res/menu/opt_image.xml
Normal file
9
app/src/main/res/menu/opt_image.xml
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user