diff --git a/.gitignore b/.gitignore
index 9b63e14e7..5611db9cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@
/.idea/kotlinScripting.xml
/.idea/deploymentTargetDropDown.xml
/.idea/androidTestResultsUserPreferences.xml
+/.idea/render.experimental.xml
.DS_Store
/build
/captures
diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml
deleted file mode 100644
index 5cad4e0be..000000000
--- a/.idea/render.experimental.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
index 8035a83fc..a1e9ace1d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt
@@ -92,6 +92,7 @@ class KotatsuApp : Application() {
ReportField.PHONE_MODEL,
ReportField.CRASH_CONFIGURATION,
ReportField.STACK_TRACE,
+ ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES,
)
dialog {
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt
index 9c85787a7..909252b48 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt
@@ -17,19 +17,28 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
-import android.widget.Button
import android.widget.FrameLayout
-import android.widget.TextView
+import androidx.annotation.ColorInt
import androidx.annotation.StringRes
+import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.postDelayed
-import org.koitharu.kotatsu.R
+import com.google.android.material.color.MaterialColors
+import com.google.android.material.shape.MaterialShapeDrawable
+import com.google.android.material.shape.ShapeAppearanceModel
+import com.google.android.material.snackbar.Snackbar
+import org.koitharu.kotatsu.databinding.FadingSnackbarLayoutBinding
+import org.koitharu.kotatsu.utils.ext.getThemeColorStateList
+import com.google.android.material.R as materialR
private const val ENTER_DURATION = 300L
private const val EXIT_DURATION = 200L
-private const val SHORT_DURATION = 1_500L
-private const val LONG_DURATION = 2_750L
+private const val SHORT_DURATION_MS = 1_500L
+private const val LONG_DURATION_MS = 2_750L
+
/**
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
*
@@ -40,16 +49,13 @@ private const val LONG_DURATION = 2_750L
class FadingSnackbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
+ defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
- private val message: TextView
- private val action: Button
+ private val binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this)
init {
- val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
- message = view.findViewById(R.id.snackbar_text)
- action = view.findViewById(R.id.snackbar_action)
+ binding.snackbarLayout.background = createThemedBackground()
}
fun dismiss() {
@@ -62,33 +68,66 @@ class FadingSnackbar @JvmOverloads constructor(
}
fun show(
- messageText: CharSequence? = null,
- @StringRes actionId: Int? = null,
- longDuration: Boolean = true,
- actionClick: () -> Unit = { dismiss() },
- dismissListener: () -> Unit = { }
+ messageText: CharSequence?,
+ @StringRes actionId: Int = 0,
+ duration: Int = Snackbar.LENGTH_SHORT,
+ onActionClick: (FadingSnackbar.() -> Unit)? = null,
+ onDismiss: (() -> Unit)? = null,
) {
- message.text = messageText
- if (actionId != null) {
- action.run {
+ binding.snackbarText.text = messageText
+ if (actionId != 0) {
+ with(binding.snackbarAction) {
visibility = VISIBLE
text = context.getString(actionId)
setOnClickListener {
- actionClick()
+ onActionClick?.invoke(this@FadingSnackbar) ?: dismiss()
}
}
} else {
- action.visibility = GONE
+ binding.snackbarAction.visibility = GONE
}
alpha = 0f
visibility = VISIBLE
animate()
.alpha(1f)
.duration = ENTER_DURATION
- val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
- postDelayed(showDuration) {
+ if (duration == Snackbar.LENGTH_INDEFINITE) {
+ return
+ }
+ val durationMs = ENTER_DURATION + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS
+ postDelayed(durationMs) {
dismiss()
- dismissListener()
+ onDismiss?.invoke()
}
}
+
+ private fun createThemedBackground(): Drawable {
+ val backgroundColor = MaterialColors.layer(this, materialR.attr.colorSurface, materialR.attr.colorOnSurface, 1f)
+ val shapeAppearanceModel = ShapeAppearanceModel.builder(
+ context,
+ materialR.style.ShapeAppearance_Material3_Corner_ExtraSmall,
+ 0
+ ).build()
+ val background = createMaterialShapeDrawableBackground(
+ backgroundColor,
+ shapeAppearanceModel,
+ )
+ val backgroundTint = context.getThemeColorStateList(materialR.attr.colorSurfaceInverse)
+ return if (backgroundTint != null) {
+ val wrappedDrawable = DrawableCompat.wrap(background)
+ DrawableCompat.setTintList(wrappedDrawable, backgroundTint)
+ wrappedDrawable
+ } else {
+ DrawableCompat.wrap(background)
+ }
+ }
+
+ private fun createMaterialShapeDrawableBackground(
+ @ColorInt backgroundColor: Int,
+ shapeAppearanceModel: ShapeAppearanceModel,
+ ): MaterialShapeDrawable {
+ val background = MaterialShapeDrawable(shapeAppearanceModel)
+ background.fillColor = ColorStateList.valueOf(backgroundColor)
+ return background
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index dc5e6a513..df529038e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -21,9 +21,11 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch
+import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
@@ -37,6 +39,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService
+import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
@@ -81,7 +84,7 @@ class DetailsActivity :
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError)
viewModel.onShowToast.observe(this) {
- binding.snackbar.show(messageText = getString(it), longDuration = false)
+ binding.snackbar.show(messageText = getString(it))
}
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
@@ -114,6 +117,21 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
+ e is ParseException || e is IllegalArgumentException || e is IllegalStateException -> {
+ binding.snackbar.show(
+ messageText = e.getDisplayMessage(resources),
+ actionId = R.string.report,
+ duration = if (viewModel.manga.value?.chapters == null) {
+ Snackbar.LENGTH_INDEFINITE
+ } else {
+ Snackbar.LENGTH_LONG
+ },
+ onActionClick = {
+ e.sendWithAcra()
+ dismiss()
+ }
+ )
+ }
else -> {
binding.snackbar.show(e.getDisplayMessage(resources))
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index 757c70f61..0cab2ea03 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
-import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
@@ -30,6 +29,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+import java.io.IOException
class DetailsViewModel(
intent: MangaIntent,
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
index 07f03dbda..c6c45ecc1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui
import androidx.core.os.LocaleListCompat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import org.acra.ACRA
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
@@ -13,6 +14,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
+import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -20,6 +22,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+import org.koitharu.kotatsu.utils.ext.setCurrentManga
class MangaDetailsDelegate(
private val intent: MangaIntent,
@@ -32,6 +35,7 @@ class MangaDetailsDelegate(
private val mangaData = MutableStateFlow(intent.manga)
val selectedBranch = MutableStateFlow(null)
+
// Remote manga for saved and saved for remote
val relatedManga = MutableStateFlow(null)
val manga: StateFlow
@@ -41,6 +45,7 @@ class MangaDetailsDelegate(
suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga")
+ ACRA.setCurrentManga(manga)
mangaData.value = manga
manga = MangaRepository(manga.source).getDetails(manga)
// find default branch
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index e4a29b948..dfd3af976 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.acra.ktx.sendWithAcra
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
@@ -214,6 +215,8 @@ class ReaderActivity :
val resolveTextId = ExceptionResolver.getResolveStringId(e)
if (resolveTextId != 0) {
dialog.setPositiveButton(resolveTextId, listener)
+ } else {
+ dialog.setPositiveButton(R.string.report, listener)
}
dialog.show()
}
@@ -368,7 +371,11 @@ class ReaderActivity :
override fun onClick(dialog: DialogInterface?, which: Int) {
if (which == DialogInterface.BUTTON_POSITIVE) {
dialog?.dismiss()
- tryResolve(exception)
+ if (ExceptionResolver.canResolve(exception)) {
+ tryResolve(exception)
+ } else {
+ exception.sendWithAcra()
+ }
} else {
onCancel(dialog)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index 931461de0..bb8555941 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -6,9 +6,9 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
-import java.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import org.acra.ACRA
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
@@ -32,6 +32,8 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
+import org.koitharu.kotatsu.utils.ext.setCurrentManga
+import java.util.*
private const val BOUNDS_PAGE_OFFSET = 2
private const val PAGES_TRIM_THRESHOLD = 120
@@ -257,6 +259,7 @@ class ReaderViewModel(
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga")
+ ACRA.setCurrentManga(manga)
mangaData.value = manga
val repo = MangaRepository(manga.source)
manga = repo.getDetails(manga)
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
similarity index 87%
rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt
rename to app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
index dc6974749..a71f3b7b3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt
@@ -1,12 +1,14 @@
package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources
+import org.acra.ACRA
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
+import org.koitharu.kotatsu.parsers.model.Manga
import java.io.FileNotFoundException
import java.net.SocketTimeoutException
@@ -20,4 +22,6 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is SocketTimeoutException -> resources.getString(R.string.network_error)
is WrongPasswordException -> resources.getString(R.string.wrong_password)
else -> localizedMessage ?: resources.getString(R.string.error_occurred)
-}
\ No newline at end of file
+}
+
+fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString())
\ No newline at end of file
diff --git a/app/src/main/res/layout/fading_snackbar_layout.xml b/app/src/main/res/layout/fading_snackbar_layout.xml
index 4c75ce8b9..0ebbb7bdd 100644
--- a/app/src/main/res/layout/fading_snackbar_layout.xml
+++ b/app/src/main/res/layout/fading_snackbar_layout.xml
@@ -1,5 +1,4 @@
-
-