Merge branch 'master' into devel
This commit is contained in:
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.medianOrNull
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@@ -59,6 +60,14 @@ object MangaUtils : KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
||||
options.outMimeType
|
||||
}
|
||||
|
||||
private fun getBitmapSize(input: InputStream?): Size {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
|
||||
@@ -9,11 +9,12 @@ import android.view.ViewGroup.LayoutParams
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||
|
||||
@@ -43,7 +44,9 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return if (resources.getBoolean(R.bool.is_tablet)) {
|
||||
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
|
||||
} else super.onCreateDialog(savedInstanceState)
|
||||
} else {
|
||||
AppBottomSheetDialog(requireContext(), theme)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.base.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
|
||||
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
|
||||
|
||||
/**
|
||||
* https://github.com/material-components/material-components-android/issues/2582
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onAttachedToWindow() {
|
||||
val window = window
|
||||
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
|
||||
super.onAttachedToWindow()
|
||||
if (window != null) {
|
||||
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
|
||||
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
|
||||
if (drawEdgeToEdge) {
|
||||
// Copied from super.onAttachedToWindow:
|
||||
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
// Fix super-class's window flag bug by respecting the intial system UI visibility:
|
||||
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.size.Scale
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
@@ -43,6 +44,7 @@ fun mangaGridItemAD(
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
.allowRgb565(true)
|
||||
.scale(Scale.FILL)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
badge = itemView.bindBadge(badge, item.counter)
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.size.Scale
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
@@ -44,6 +45,7 @@ fun mangaListDetailedItemAD(
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
.scale(Scale.FILL)
|
||||
.allowRgb565(true)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.size.Scale
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
@@ -44,6 +45,7 @@ fun mangaListItemAD(
|
||||
.placeholder(R.drawable.ic_placeholder)
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
.scale(Scale.FILL)
|
||||
.allowRgb565(true)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -418,20 +417,19 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun onFirstStart() {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
if (AppUpdateChecker.isUpdateSupported(this@MainActivity)) {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val isUpdateSupported = withContext(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
AppUpdateChecker.isUpdateSupported(this@MainActivity)
|
||||
}
|
||||
if (isUpdateSupported) {
|
||||
AppUpdateChecker(this@MainActivity).checkIfNeeded()
|
||||
}
|
||||
val settings = get<AppSettings>()
|
||||
when {
|
||||
!settings.isSourcesSelected -> withContext(Dispatchers.Main) {
|
||||
OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||
}
|
||||
settings.newSources.isNotEmpty() -> withContext(Dispatchers.Main) {
|
||||
NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,26 @@ package org.koitharu.kotatsu.reader.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.base.domain.MangaUtils
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import java.io.File
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val MAX_FILENAME_LENGTH = 10
|
||||
private const val EXTENSION_FALLBACK = "png"
|
||||
|
||||
class PageSaveHelper(
|
||||
private val cache: PagesCache,
|
||||
context: Context,
|
||||
@@ -28,22 +35,17 @@ class PageSaveHelper(
|
||||
page: MangaPage,
|
||||
saveLauncher: ActivityResultLauncher<String>,
|
||||
): Uri {
|
||||
var pageFile = cache[page.url]
|
||||
var fileName = pageFile?.name
|
||||
if (fileName == null) {
|
||||
fileName = pageLoader.getPageUrl(page).toHttpUrl().pathSegments.last()
|
||||
}
|
||||
val cc = coroutineContext
|
||||
val destination = suspendCancellableCoroutine<Uri> { cont ->
|
||||
continuation = cont
|
||||
Dispatchers.Main.dispatch(cc) {
|
||||
saveLauncher.launch(fileName)
|
||||
val pageUrl = pageLoader.getPageUrl(page)
|
||||
val pageFile = pageLoader.loadPage(page, force = false)
|
||||
val proposedName = getProposedFileName(pageUrl, pageFile)
|
||||
val destination = withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine<Uri> { cont ->
|
||||
continuation = cont
|
||||
saveLauncher.launch(proposedName)
|
||||
}.also {
|
||||
continuation = null
|
||||
}
|
||||
}
|
||||
continuation = null
|
||||
if (pageFile == null) {
|
||||
pageFile = pageLoader.loadPage(page, force = false)
|
||||
}
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(destination)?.use { output ->
|
||||
pageFile.inputStream().use { input ->
|
||||
@@ -57,4 +59,19 @@ class PageSaveHelper(
|
||||
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
|
||||
resume(uri)
|
||||
} != null
|
||||
|
||||
private suspend fun getProposedFileName(url: String, file: File): String {
|
||||
var name = url.toHttpUrl().pathSegments.last()
|
||||
var extension = name.substringAfterLast('.', "")
|
||||
name = name.substringBeforeLast('.')
|
||||
if (extension.length !in 2..4) {
|
||||
val mimeType = MangaUtils.getImageMimeType(file)
|
||||
extension = if (mimeType != null) {
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||
} else {
|
||||
EXTENSION_FALLBACK
|
||||
}
|
||||
}
|
||||
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,15 @@ import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
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.koin.android.ext.android.get
|
||||
@@ -19,15 +28,6 @@ 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 java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
@@ -61,25 +61,22 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
|
||||
@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)
|
||||
.setTitle(R.string.app_update_available)
|
||||
.setMessage(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)
|
||||
})
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
|
||||
}
|
||||
.setNegativeButton(R.string.close, null)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
@@ -128,4 +125,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,10 @@ import org.koitharu.kotatsu.utils.ext.observeNotNull
|
||||
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(),
|
||||
OnListItemClickListener<SourceLocale>, DialogInterface.OnClickListener {
|
||||
class OnboardDialogFragment :
|
||||
AlertDialogFragment<DialogOnboardBinding>(),
|
||||
OnListItemClickListener<SourceLocale>,
|
||||
DialogInterface.OnClickListener {
|
||||
|
||||
private val viewModel by viewModel<OnboardViewModel>()
|
||||
private var isWelcome: Boolean = false
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.onboard
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||
import org.koitharu.kotatsu.utils.ext.map
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
import java.util.*
|
||||
|
||||
class OnboardViewModel(
|
||||
private val settings: AppSettings,
|
||||
@@ -55,6 +55,7 @@ class OnboardViewModel(
|
||||
settings.hiddenSources = allSources.filterNot { x ->
|
||||
x.locale in selectedLocales
|
||||
}.mapToSet { x -> x.name }
|
||||
settings.markKnownSources(settings.newSources)
|
||||
}
|
||||
|
||||
private fun rebuildList() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker.ui.adapter
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.size.Scale
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
@@ -34,6 +35,7 @@ fun feedItemAD(
|
||||
.fallback(R.drawable.ic_placeholder)
|
||||
.error(R.drawable.ic_placeholder)
|
||||
.allowRgb565(true)
|
||||
.scale(Scale.FILL)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
binding.textViewTitle.text = item.title
|
||||
|
||||
@@ -2,15 +2,16 @@ package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
open class BottomSheetToolbarController(
|
||||
protected val toolbar: Toolbar,
|
||||
) : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && bottomSheet.top <= 0
|
||||
if (isExpanded) {
|
||||
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
|
||||
} else {
|
||||
toolbar.navigationIcon = null
|
||||
|
||||
Reference in New Issue
Block a user