Merge branch 'devel' into feature/colorfilter
commit
6a0a4023ad
@ -0,0 +1,72 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.core.text.htmlEncode
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.databinding.DialogMangaErrorBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.report
|
||||||
|
import org.koitharu.kotatsu.utils.ext.requireParcelable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.requireSerializable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
|
class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
|
||||||
|
|
||||||
|
private lateinit var error: Throwable
|
||||||
|
private lateinit var manga: Manga
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val args = requireArguments()
|
||||||
|
manga = args.requireParcelable<ParcelableManga>(ARG_MANGA).manga
|
||||||
|
error = args.requireSerializable(ARG_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogMangaErrorBinding {
|
||||||
|
return DialogMangaErrorBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
with(binding.textViewMessage) {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
text = context.getString(
|
||||||
|
R.string.manga_error_description_pattern,
|
||||||
|
this@MangaErrorDialog.error.message?.htmlEncode().orEmpty(),
|
||||||
|
manga.publicUrl,
|
||||||
|
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||||
|
return super.onBuildDialog(builder)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.report) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
error.report(TAG)
|
||||||
|
}.setTitle(R.string.error_occurred)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "MangaErrorDialog"
|
||||||
|
private const val ARG_ERROR = "error"
|
||||||
|
private const val ARG_MANGA = "manga"
|
||||||
|
|
||||||
|
fun show(fm: FragmentManager, manga: Manga, error: Throwable) = MangaErrorDialog().withArgs(2) {
|
||||||
|
putParcelable(ARG_MANGA, ParcelableManga(manga, false))
|
||||||
|
putSerializable(ARG_ERROR, error)
|
||||||
|
}.show(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,110 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
|
||||||
import org.koitharu.kotatsu.core.github.AppVersion
|
|
||||||
import org.koitharu.kotatsu.core.github.VersionId
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
|
||||||
import org.koitharu.kotatsu.utils.FileSize
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
class AppUpdateChecker(private val activity: ComponentActivity) {
|
|
||||||
|
|
||||||
private val settings: AppSettings = TODO()
|
|
||||||
private val repo: AppUpdateRepository = TODO()
|
|
||||||
|
|
||||||
suspend fun checkIfNeeded(): Boolean? = if (
|
|
||||||
settings.isUpdateCheckingEnabled &&
|
|
||||||
settings.lastUpdateCheckTimestamp + PERIOD < System.currentTimeMillis()
|
|
||||||
) {
|
|
||||||
checkNow()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkNow() = runCatching {
|
|
||||||
val version = repo.fetchUpdate() ?: return@runCatching false
|
|
||||||
val newVersionId = VersionId(version.name)
|
|
||||||
val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
|
|
||||||
val result = newVersionId > currentVersionId
|
|
||||||
if (result) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
showUpdateDialog(version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings.lastUpdateCheckTimestamp = System.currentTimeMillis()
|
|
||||||
result
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
private fun showUpdateDialog(version: AppVersion) {
|
|
||||||
val message = buildString {
|
|
||||||
append(activity.getString(R.string.new_version_s, version.name))
|
|
||||||
appendLine()
|
|
||||||
append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize)))
|
|
||||||
appendLine()
|
|
||||||
appendLine()
|
|
||||||
append(version.description)
|
|
||||||
}
|
|
||||||
MaterialAlertDialogBuilder(activity, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
|
|
||||||
.setTitle(R.string.app_update_available)
|
|
||||||
.setMessage(message)
|
|
||||||
.setIcon(R.drawable.ic_app_update)
|
|
||||||
.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
|
|
||||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.close, null)
|
|
||||||
.setCancelable(false)
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
|
|
||||||
private val PERIOD = TimeUnit.HOURS.toMillis(6)
|
|
||||||
|
|
||||||
fun isUpdateSupported(context: Context): Boolean {
|
|
||||||
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
@SuppressLint("PackageManagerGetSignatures")
|
|
||||||
private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching {
|
|
||||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
|
||||||
val signatures = requireNotNull(packageInfo?.signatures)
|
|
||||||
val cert: ByteArray = signatures.first().toByteArray()
|
|
||||||
val input: InputStream = ByteArrayInputStream(cert)
|
|
||||||
val cf = CertificateFactory.getInstance("X509")
|
|
||||||
val c = cf.generateCertificate(input) as X509Certificate
|
|
||||||
val md: MessageDigest = MessageDigest.getInstance("SHA1")
|
|
||||||
val publicKey: ByteArray = md.digest(c.encoded)
|
|
||||||
return publicKey.byte2HexFormatted()
|
|
||||||
}.onFailure { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.about
|
||||||
|
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||||
|
import org.koitharu.kotatsu.core.github.AppVersion
|
||||||
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AboutSettingsViewModel @Inject constructor(
|
||||||
|
private val appUpdateRepository: AppUpdateRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val isUpdateSupported = appUpdateRepository.isUpdateSupported()
|
||||||
|
val onUpdateAvailable = SingleLiveEvent<AppVersion?>()
|
||||||
|
|
||||||
|
fun checkForUpdates() {
|
||||||
|
launchLoadingJob {
|
||||||
|
val update = appUpdateRepository.fetchUpdate()
|
||||||
|
onUpdateAvailable.call(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.about
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.github.AppVersion
|
||||||
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
|
|
||||||
|
class AppUpdateDialog(private val context: Context) {
|
||||||
|
|
||||||
|
fun show(version: AppVersion) {
|
||||||
|
val message = buildString {
|
||||||
|
append(context.getString(R.string.new_version_s, version.name))
|
||||||
|
appendLine()
|
||||||
|
append(context.getString(R.string.size_s, FileSize.BYTES.format(context, version.apkSize)))
|
||||||
|
appendLine()
|
||||||
|
appendLine()
|
||||||
|
append(version.description)
|
||||||
|
}
|
||||||
|
MaterialAlertDialogBuilder(
|
||||||
|
context,
|
||||||
|
materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
|
||||||
|
)
|
||||||
|
.setTitle(R.string.app_update_available)
|
||||||
|
.setMessage(message)
|
||||||
|
.setIcon(R.drawable.ic_app_update)
|
||||||
|
.setPositiveButton(R.string.download) { _, _ ->
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri())
|
||||||
|
context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser)))
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.close, null)
|
||||||
|
.setCancelable(false)
|
||||||
|
.create()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
|
class IdlingDetector(
|
||||||
|
private val timeoutMs: Long,
|
||||||
|
private val callback: Callback,
|
||||||
|
) : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val idleRunnable = Runnable {
|
||||||
|
callback.onIdle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindToLifecycle(owner: LifecycleOwner) {
|
||||||
|
owner.lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUserInteraction() {
|
||||||
|
handler.removeCallbacks(idleRunnable)
|
||||||
|
handler.postDelayed(idleRunnable, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
super.onDestroy(owner)
|
||||||
|
owner.lifecycle.removeObserver(this)
|
||||||
|
handler.removeCallbacks(idleRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Callback {
|
||||||
|
|
||||||
|
fun onIdle()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getParcelable(key, T::class.java)
|
||||||
|
} else {
|
||||||
|
getParcelable(key) as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getParcelableExtra(key, T::class.java)
|
||||||
|
} else {
|
||||||
|
getParcelableExtra(key) as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getSerializable(key, T::class.java)
|
||||||
|
} else {
|
||||||
|
getSerializable(key) as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T {
|
||||||
|
return checkNotNull(getSerializableCompat(key)) {
|
||||||
|
"Serializable of type \"${T::class.java.name}\" not found at \"$key\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Parcelable> Bundle.requireParcelable(key: String): T {
|
||||||
|
return checkNotNull(getParcelableCompat(key)) {
|
||||||
|
"Parcelable of type \"${T::class.java.name}\" not found at \"$key\""
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="@dimen/margin_normal"
|
||||||
|
android:paddingTop="@dimen/margin_normal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_message"
|
||||||
|
style="@style/MaterialAlertDialog.Material3.Body.Text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:linksClickable="true"
|
||||||
|
tools:text="@tools:sample/lorem[20]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue