Compare commits
80 Commits
feature/ki
...
v5.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| f1f208ad15 | |||
| c6983d794c | |||
| 8228153c83 | |||
| 844bd13a07 | |||
| 60a5620134 | |||
|
|
dd09a39077 | ||
|
|
1511bd3279 | ||
|
|
259c335607 | ||
|
|
86367b6d3b | ||
| 19b893738d | |||
| d817ae0394 | |||
| d81c22b586 | |||
| cd23b044df | |||
| 4922881343 | |||
| ff0d04bea6 | |||
| 97de629c3b | |||
| 7b482e5bcf | |||
| fd575b8131 | |||
| c77e023bef | |||
| a3cf52859b | |||
| 5e55bce529 | |||
| b1ba70bf77 | |||
| b930272221 | |||
| 75305c0b94 | |||
| 24b16e2ce2 | |||
|
|
0ccbba6787 | ||
|
|
ca314867f2 | ||
|
|
236e284360 | ||
|
|
e9a09b6be4 | ||
|
|
9e1be337ed | ||
|
|
104f2ebfae | ||
| 6a2e12dc29 | |||
| 9587cb439c | |||
| c42d0824b0 | |||
| 09f6dd9b4e | |||
| b494c96e31 | |||
| 0f6d56ee2d | |||
| 8d15691e17 | |||
|
|
bd8b251934 | ||
|
|
2f1b74e45a | ||
|
|
73217b8e11 | ||
|
|
759df969c9 | ||
|
|
466e35fffa | ||
|
|
f44db3dbff | ||
|
|
315870abcb | ||
|
|
3e46b3957c | ||
|
|
6dc81468d2 | ||
|
|
56bc0dbf07 | ||
|
|
7bc33adca8 | ||
|
|
c8794d59f7 | ||
|
|
9c2a57812e | ||
|
|
6bd5033858 | ||
|
|
e7c2a76219 | ||
|
|
0934363298 | ||
|
|
de29527805 | ||
|
|
f11e964f0b | ||
|
|
61a98f54b9 | ||
|
|
50e67daea4 | ||
|
|
0030706226 | ||
|
|
056ef5433d | ||
|
|
c14b2ceeff | ||
|
|
ff2cf9d18a | ||
|
|
96b6900c70 | ||
|
|
c6228d3fe1 | ||
|
|
8ac95e1608 | ||
|
|
69a9ec354b | ||
|
|
0639d3e6c1 | ||
|
|
ae5cebd42d | ||
|
|
cd8381cbfb | ||
|
|
3132049a63 | ||
| bc3a7fc211 | |||
| e794f84c6f | |||
| 76709dda21 | |||
| 6dc460bc20 | |||
| c2ee548f0a | |||
| 1847759ec3 | |||
| 02d5dfb375 | |||
| ef9b16da0b | |||
| 5d1ef983e9 | |||
| eb78a776cf |
3
.weblate
Normal file
3
.weblate
Normal file
@@ -0,0 +1,3 @@
|
||||
[weblate]
|
||||
url = https://hosted.weblate.org/api/
|
||||
translation = kotatsu/strings
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 547
|
||||
versionName '5.1.3'
|
||||
versionCode 554
|
||||
versionName '5.2.2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -79,17 +79,17 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
|
||||
@@ -104,6 +104,7 @@ dependencies {
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
||||
|
||||
// TODO https://issuetracker.google.com/issues/254846063
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
//noinspection GradleDependency
|
||||
implementation('com.google.guava:guava:32.0.0-android') {
|
||||
|
||||
@@ -23,7 +23,7 @@ import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ShortcutsUpdaterTest {
|
||||
class AppShortcutManagerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
@@ -32,7 +32,7 @@ class ShortcutsUpdaterTest {
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
@Inject
|
||||
lateinit var database: MangaDatabase
|
||||
@@ -72,6 +72,6 @@ class ShortcutsUpdaterTest {
|
||||
private suspend fun awaitUpdate() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
instrumentation.awaitForIdle()
|
||||
shortcutsUpdater.await()
|
||||
appShortcutManager.await()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.util
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.util.Log
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||
@@ -1,3 +1,3 @@
|
||||
package org.koitharu.kotatsu.util.ext
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||
@@ -102,6 +102,10 @@
|
||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.work.Configuration
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.httpSender
|
||||
@@ -19,6 +20,7 @@ import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.os.AppValidator
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
@@ -26,6 +28,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
@@ -46,8 +49,15 @@ class KotatsuApp : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
@Inject
|
||||
lateinit var appValidator: AppValidator
|
||||
|
||||
@Inject
|
||||
lateinit var workScheduleManager: WorkScheduleManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
|
||||
if (BuildConfig.DEBUG) {
|
||||
enableStrictMode()
|
||||
}
|
||||
@@ -57,6 +67,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
setupDatabaseObservers()
|
||||
}
|
||||
workScheduleManager.init()
|
||||
WorkServiceStopHelper(applicationContext).setup()
|
||||
}
|
||||
|
||||
@@ -67,6 +78,10 @@ class KotatsuApp : Application(), Configuration.Provider {
|
||||
reportFormat = StringFormat.JSON
|
||||
excludeMatchingSharedPreferencesKeys = listOf(
|
||||
"sources_\\w+",
|
||||
AppSettings.KEY_APP_PASSWORD,
|
||||
AppSettings.KEY_PROXY_LOGIN,
|
||||
AppSettings.KEY_PROXY_ADDRESS,
|
||||
AppSettings.KEY_PROXY_PASSWORD,
|
||||
)
|
||||
httpSender {
|
||||
uri = getString(R.string.url_error_report)
|
||||
@@ -83,8 +98,10 @@ class KotatsuApp : Application(), Configuration.Provider {
|
||||
ReportField.PHONE_MODEL,
|
||||
ReportField.STACK_TRACE,
|
||||
ReportField.CRASH_CONFIGURATION,
|
||||
ReportField.CUSTOM_DATA,
|
||||
ReportField.SHARED_PREFERENCES,
|
||||
)
|
||||
|
||||
dialog {
|
||||
text = getString(R.string.crash_text)
|
||||
title = getString(R.string.error_occurred)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.bookmarks.domain
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import java.util.Date
|
||||
|
||||
class Bookmark(
|
||||
@@ -14,6 +15,20 @@ class Bookmark(
|
||||
val percent: Float,
|
||||
) {
|
||||
|
||||
val directImageUrl: String?
|
||||
get() = if (isImageUrlDirect()) imageUrl else null
|
||||
|
||||
fun toMangaPage() = MangaPage(
|
||||
id = pageId,
|
||||
url = imageUrl,
|
||||
preview = null,
|
||||
source = manga.source,
|
||||
)
|
||||
|
||||
private fun isImageUrlDirect(): Boolean {
|
||||
return imageUrl.substringAfterLast('.').length in 2..4
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -15,8 +15,8 @@ import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
|
||||
@@ -95,8 +95,11 @@ class BookmarksFragment :
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
||||
val intent = ReaderActivity.newIntent(view.context, item)
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||
val intent = ReaderActivity.IntentBuilder(view.context)
|
||||
.bookmark(item)
|
||||
.incognito(true)
|
||||
.build()
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view))
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ fun bookmarkListAD(
|
||||
binding.root.setOnLongClickListener(listener)
|
||||
|
||||
bind {
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
|
||||
val data: Any = item.directImageUrl ?: item.toMangaPage()
|
||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
|
||||
size(CoverSizeResolver(binding.imageViewThumb))
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(R.drawable.ic_placeholder)
|
||||
|
||||
@@ -103,6 +103,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
viewBinding.webView.stopLoading()
|
||||
viewBinding.webView.destroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCallback {
|
||||
|
||||
private var pendingResult = RESULT_CANCELED
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
private var onBackPressedCallback: WebViewBackPressedCallback? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||
return
|
||||
}
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
val url = intent?.dataString.orEmpty()
|
||||
with(viewBinding.webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
|
||||
}
|
||||
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
||||
onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
if (savedInstanceState != null) {
|
||||
return
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
finishAfterTransition()
|
||||
} else {
|
||||
onTitleChanged(getString(R.string.loading_), url)
|
||||
viewBinding.webView.loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
viewBinding.webView.run {
|
||||
stopLoading()
|
||||
destroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
viewBinding.webView.saveState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
viewBinding.webView.restoreState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.appbar.updatePadding(
|
||||
top = insets.top,
|
||||
)
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
viewBinding.webView.stopLoading()
|
||||
finishAfterTransition()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewBinding.webView.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewBinding.webView.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
setResult(pendingResult)
|
||||
super.finish()
|
||||
}
|
||||
|
||||
override fun onPageLoaded() {
|
||||
viewBinding.progressBar.isInvisible = true
|
||||
}
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult = RESULT_OK
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
viewBinding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
override fun onHistoryChanged() {
|
||||
onBackPressedCallback?.onHistoryChanged()
|
||||
}
|
||||
|
||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||
setTitle(title)
|
||||
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
||||
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent {
|
||||
return newIntent(context, input.first, input.second)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||
return TaggedActivityResult(TAG, resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareActivity"
|
||||
private const val ARG_UA = "ua"
|
||||
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
url: String,
|
||||
headers: Headers?,
|
||||
) = Intent(context, CloudFlareActivity::class.java).apply {
|
||||
data = url.toUri()
|
||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||
putExtra(ARG_UA, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package org.koitharu.kotatsu.browser.cloudflare
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||
|
||||
private lateinit var url: String
|
||||
private val pendingResult = Bundle(1)
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
private var onBackPressedCallback: WebViewBackPressedCallback? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
url = requireArguments().getString(ARG_URL).orEmpty()
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentCloudflareBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
with(binding.webView.settings) {
|
||||
javaScriptEnabled = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome
|
||||
}
|
||||
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
||||
if (url.isEmpty()) {
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
binding.webView.loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
requireViewBinding().webView.stopLoading()
|
||||
requireViewBinding().webView.destroy()
|
||||
onBackPressedCallback = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
||||
return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
|
||||
}
|
||||
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
super.onDialogCreated(dialog)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(requireViewBinding().webView).also {
|
||||
dialog.onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireViewBinding().webView.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
requireViewBinding().webView.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
setFragmentResult(TAG, pendingResult)
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
override fun onPageLoaded() {
|
||||
viewBinding?.progressBar?.isInvisible = true
|
||||
}
|
||||
|
||||
override fun onCheckPassed() {
|
||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onHistoryChanged() {
|
||||
onBackPressedCallback?.onHistoryChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG = "CloudFlareDialog"
|
||||
const val EXTRA_RESULT = "result"
|
||||
private const val ARG_URL = "url"
|
||||
private const val ARG_UA = "ua"
|
||||
|
||||
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
|
||||
putString(ARG_URL, url)
|
||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||
putString(ARG_UA, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,18 @@ import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.network.*
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
||||
import org.koitharu.kotatsu.core.util.ext.activityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
@@ -65,9 +64,6 @@ interface AppModule {
|
||||
@Binds
|
||||
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
|
||||
|
||||
@Binds
|
||||
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@@ -130,12 +126,12 @@ interface AppModule {
|
||||
@ElementsIntoSet
|
||||
fun provideDatabaseObservers(
|
||||
widgetUpdater: WidgetUpdater,
|
||||
shortcutsUpdater: ShortcutsUpdater,
|
||||
appShortcutManager: AppShortcutManager,
|
||||
backupObserver: BackupObserver,
|
||||
syncController: SyncController,
|
||||
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
|
||||
widgetUpdater,
|
||||
shortcutsUpdater,
|
||||
appShortcutManager,
|
||||
backupObserver,
|
||||
syncController,
|
||||
)
|
||||
@@ -146,10 +142,12 @@ interface AppModule {
|
||||
appProtectHelper: AppProtectHelper,
|
||||
activityRecreationHandle: ActivityRecreationHandle,
|
||||
incognitoModeIndicator: IncognitoModeIndicator,
|
||||
acraScreenLogger: AcraScreenLogger,
|
||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||
appProtectHelper,
|
||||
activityRecreationHandle,
|
||||
incognitoModeIndicator,
|
||||
acraScreenLogger,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -6,15 +6,13 @@ import androidx.annotation.StringRes
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
|
||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.core.util.isSuccess
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -23,20 +21,26 @@ import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class ExceptionResolver private constructor(
|
||||
private val activity: FragmentActivity?,
|
||||
private val fragment: Fragment?,
|
||||
) : ActivityResultCallback<TaggedActivityResult> {
|
||||
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
|
||||
|
||||
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
||||
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||
private val activity: FragmentActivity?
|
||||
private val fragment: Fragment?
|
||||
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
|
||||
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>>
|
||||
|
||||
constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) {
|
||||
constructor(activity: FragmentActivity) {
|
||||
this.activity = activity
|
||||
fragment = null
|
||||
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
||||
cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this)
|
||||
}
|
||||
|
||||
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
|
||||
constructor(fragment: Fragment) {
|
||||
this.fragment = fragment
|
||||
activity = null
|
||||
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
||||
cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: TaggedActivityResult) {
|
||||
@@ -58,22 +62,9 @@ class ExceptionResolver private constructor(
|
||||
else -> false
|
||||
}
|
||||
|
||||
private suspend fun resolveCF(url: String, headers: Headers): Boolean {
|
||||
val dialog = CloudFlareDialog.newInstance(url, headers)
|
||||
val fm = getFragmentManager()
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
||||
continuations[CloudFlareDialog.TAG] = cont
|
||||
fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
|
||||
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
|
||||
}
|
||||
dialog.show(fm, CloudFlareDialog.TAG)
|
||||
cont.invokeOnCancellation {
|
||||
continuations.remove(CloudFlareDialog.TAG, cont)
|
||||
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
||||
dialog.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
|
||||
continuations[CloudFlareActivity.TAG] = cont
|
||||
cloudflareContract.launch(url to headers)
|
||||
}
|
||||
|
||||
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -14,28 +10,22 @@ import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.os.AppValidator
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.asArrayList
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
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 const val CONTENT_TYPE_APK = "application/vnd.android.package-archive"
|
||||
|
||||
@Singleton
|
||||
class AppUpdateRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appValidator: AppValidator,
|
||||
private val settings: AppSettings,
|
||||
@BaseHttpClient private val okHttp: OkHttpClient,
|
||||
) {
|
||||
@@ -85,7 +75,7 @@ class AppUpdateRepository @Inject constructor(
|
||||
}
|
||||
|
||||
fun isUpdateSupported(): Boolean {
|
||||
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1
|
||||
return BuildConfig.DEBUG || appValidator.isOriginalApp
|
||||
}
|
||||
|
||||
suspend fun getCurrentVersionChangelog(): String? {
|
||||
@@ -94,22 +84,6 @@ class AppUpdateRepository @Inject constructor(
|
||||
return available.find { x -> x.versionId == currentVersion }?.description
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getCertificateSHA1Fingerprint(): 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()
|
||||
|
||||
private inline fun JSONArray.find(predicate: (JSONObject) -> Boolean): JSONObject? {
|
||||
val size = length()
|
||||
for (i in 0 until size) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.subdir
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
@@ -41,6 +41,10 @@ class ParcelableManga(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableManga(manga=$manga, withChapters=$withChapters)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableManga {
|
||||
return ParcelableManga(parcel)
|
||||
|
||||
@@ -23,6 +23,10 @@ class ParcelableMangaChapters(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaChapters(chapters=$chapters)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
|
||||
return ParcelableMangaChapters(parcel)
|
||||
@@ -32,4 +36,4 @@ class ParcelableMangaChapters(
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ class ParcelableMangaPages(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaPages(pages=$pages)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
|
||||
return ParcelableMangaPages(parcel)
|
||||
@@ -32,4 +36,4 @@ class ParcelableMangaPages(
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ class ParcelableMangaTags(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaTags(tags=$tags)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
|
||||
return ParcelableMangaTags(parcel)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
|
||||
@@ -13,13 +13,13 @@ private const val SERVER_CLOUDFLARE = "cloudflare"
|
||||
class CloudFlareInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
||||
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
|
||||
val request = response.request
|
||||
response.closeQuietly()
|
||||
throw CloudFlareProtectedException(
|
||||
url = response.request.url.toString(),
|
||||
url = request.url.toString(),
|
||||
headers = request.headers,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mergeWith
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.net.IDN
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -6,7 +6,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
|
||||
private const val PREFS_NAME = "cookies"
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.room.InvalidationTracker
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Precision
|
||||
import coil.size.Scale
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -25,18 +24,19 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
|
||||
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ShortcutsUpdater @Inject constructor(
|
||||
class AppShortcutManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val coil: ImageLoader,
|
||||
private val historyRepository: HistoryRepository,
|
||||
@@ -128,8 +128,8 @@ class ShortcutsUpdater @Inject constructor(
|
||||
.data(manga.coverUrl)
|
||||
.size(iconSize.width, iconSize.height)
|
||||
.tag(manga.source)
|
||||
.precision(Precision.EXACT)
|
||||
.scale(Scale.FILL)
|
||||
.transformations(ThumbnailTransformation())
|
||||
.build(),
|
||||
).getDrawableOrThrow().toBitmap()
|
||||
}.fold(
|
||||
@@ -142,8 +142,9 @@ class ShortcutsUpdater @Inject constructor(
|
||||
.setLongLabel(manga.title)
|
||||
.setIcon(icon)
|
||||
.setIntent(
|
||||
ReaderActivity.newIntent(context, manga.id)
|
||||
.setAction(ReaderActivity.ACTION_MANGA_READ),
|
||||
ReaderActivity.IntentBuilder(context)
|
||||
.mangaId(manga.id)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppValidator @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
val isOriginalApp by lazy {
|
||||
getCertificateSHA1Fingerprint() == CERT_SHA1
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
private fun getCertificateSHA1Fingerprint(): 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()
|
||||
|
||||
private 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"
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorRes
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class MangaTagHighlighter @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) {
|
||||
|
||||
private val dict by lazy {
|
||||
context.resources.openRawResource(R.raw.tags_redlist).use {
|
||||
val set = HashSet<String>()
|
||||
it.bufferedReader().forEachLine { x ->
|
||||
val line = x.trim()
|
||||
if (line.isNotEmpty()) {
|
||||
set.add(line)
|
||||
}
|
||||
}
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
fun getTint(tag: MangaTag): Int {
|
||||
return if (tag.title.lowercase() in dict) {
|
||||
R.color.warning
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||
.setView(binding.root)
|
||||
.run(::onBuildDialog)
|
||||
.create()
|
||||
.also(::onDialogCreated)
|
||||
}
|
||||
|
||||
final override fun onCreateView(
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
@@ -45,7 +44,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
@JvmField
|
||||
val actionModeDelegate = ActionModeDelegate()
|
||||
@@ -62,6 +61,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
insetsDelegate.handleImeInsets = true
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
putDataToExtras(intent)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
ActivityCompat.recreate(this)
|
||||
// ActivityCompat.recreate(this)
|
||||
error("Test")
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
|
||||
@@ -26,7 +26,7 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
protected val actionModeDelegate: ActionModeDelegate
|
||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
||||
@@ -44,11 +44,13 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
onViewBindingCreated(requireViewBinding(), savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
BaseActivity<B>() {
|
||||
@@ -18,7 +20,11 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
with(window) {
|
||||
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
|
||||
} else {
|
||||
Color.TRANSPARENT
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
|
||||
@@ -12,10 +12,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
@AndroidEntryPoint
|
||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
PreferenceFragmentCompat(),
|
||||
@@ -26,27 +26,28 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = listView
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.setBackgroundColor(view.context.getThemeColor(android.R.attr.colorBackground))
|
||||
listView.clipToPadding = false
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (titleId != 0) {
|
||||
setTitle(getString(titleId))
|
||||
}
|
||||
setTitle(if (titleId != 0) getString(titleId) else null)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@@ -56,9 +57,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
protected fun setTitle(title: CharSequence) {
|
||||
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
|
||||
?: activity?.setTitle(title)
|
||||
protected fun setTitle(title: CharSequence?) {
|
||||
(activity as? SettingsActivity)?.setSectionTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.util.ext.EventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
|
||||
abstract class CoroutineIntentService : BaseService() {
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.media.ThumbnailUtils
|
||||
import coil.size.Size
|
||||
import coil.size.pxOrElse
|
||||
import coil.transform.Transformation
|
||||
|
||||
class ThumbnailTransformation : Transformation {
|
||||
|
||||
override val cacheKey: String = javaClass.name
|
||||
|
||||
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||
return ThumbnailUtils.extractThumbnail(
|
||||
input,
|
||||
size.width.pxOrElse { input.width },
|
||||
size.height.pxOrElse { input.height },
|
||||
)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is ThumbnailTransformation
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.*
|
||||
@@ -24,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.isLayoutReversed
|
||||
import org.koitharu.kotatsu.core.util.ext.parents
|
||||
import org.koitharu.kotatsu.databinding.FastScrollerBinding
|
||||
import kotlin.math.roundToInt
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -115,6 +117,9 @@ class FastScroller @JvmOverloads constructor(
|
||||
return viewHeight * proportion
|
||||
}
|
||||
|
||||
val isScrollbarVisible: Boolean
|
||||
get() = binding.scrollbar.isVisible
|
||||
|
||||
init {
|
||||
clipChildren = false
|
||||
orientation = HORIZONTAL
|
||||
@@ -165,7 +170,9 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false
|
||||
if (!isScrollbarVisible || event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) {
|
||||
return false
|
||||
}
|
||||
|
||||
requestDisallowInterceptTouchEvent(true)
|
||||
setHandleSelected(true)
|
||||
@@ -296,10 +303,12 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
if (parent is ViewGroup) {
|
||||
setLayoutParams(parent as ViewGroup)
|
||||
} else if (recyclerView.parent is ViewGroup) {
|
||||
val viewGroup = recyclerView.parent as ViewGroup
|
||||
viewGroup.addView(this)
|
||||
setLayoutParams(viewGroup)
|
||||
} else {
|
||||
val viewGroup = findValidParent(recyclerView)
|
||||
if (viewGroup != null) {
|
||||
viewGroup.addView(this)
|
||||
setLayoutParams(viewGroup)
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener)
|
||||
@@ -513,6 +522,14 @@ class FastScroller @JvmOverloads constructor(
|
||||
return BubbleSize.values().getOrNull(ordinal) ?: defaultValue
|
||||
}
|
||||
|
||||
private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p ->
|
||||
if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) {
|
||||
p as ViewGroup
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val BubbleSize.textSize
|
||||
@Px get() = resources.getDimension(textSizeId)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -36,7 +35,7 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher
|
||||
get() = requireComponentDialog().onBackPressedDispatcher
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
||||
@@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
get() = activeActionMode != null
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
activeActionMode?.finish()
|
||||
finishActionMode()
|
||||
}
|
||||
|
||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
@@ -45,6 +45,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
|
||||
}
|
||||
|
||||
fun finishActionMode() {
|
||||
activeActionMode?.finish()
|
||||
}
|
||||
|
||||
private inner class ListenerLifecycleObserver(
|
||||
private val listener: ActionModeListener,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
|
||||
fun interface ReversibleHandle {
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import java.util.LinkedList
|
||||
|
||||
class WindowInsetsDelegate(
|
||||
private val listener: WindowInsetsListener,
|
||||
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
|
||||
class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
|
||||
|
||||
@JvmField
|
||||
var handleImeInsets: Boolean = false
|
||||
@@ -16,6 +15,7 @@ class WindowInsetsDelegate(
|
||||
@JvmField
|
||||
var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null
|
||||
|
||||
private val listeners = LinkedList<WindowInsetsListener>()
|
||||
private var lastInsets: Insets? = null
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
@@ -29,7 +29,7 @@ class WindowInsetsDelegate(
|
||||
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
if (newInsets != lastInsets) {
|
||||
listener.onWindowInsetsChanged(newInsets)
|
||||
listeners.forEach { it.onWindowInsetsChanged(newInsets) }
|
||||
lastInsets = newInsets
|
||||
}
|
||||
return handledInsets
|
||||
@@ -52,6 +52,15 @@ class WindowInsetsDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
fun addInsetsListener(listener: WindowInsetsListener) {
|
||||
listeners.add(listener)
|
||||
lastInsets?.let { listener.onWindowInsetsChanged(it) }
|
||||
}
|
||||
|
||||
fun removeInsetsListener(listener: WindowInsetsListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||
view.addOnLayoutChangeListener(this)
|
||||
|
||||
@@ -92,6 +92,14 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun showOrHide(show: Boolean) {
|
||||
if (show) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
|
||||
currentAnimator = animate()
|
||||
.translationY(targetY)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
|
||||
import org.acra.ACRA
|
||||
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.WeakHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AcraScreenLogger @Inject constructor() : FragmentLifecycleCallbacks(), DefaultActivityLifecycleCallbacks {
|
||||
|
||||
private val timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.ROOT)
|
||||
private val keys = WeakHashMap<Any, String>()
|
||||
|
||||
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
|
||||
super.onFragmentAttached(fm, f, context)
|
||||
ACRA.errorReporter.putCustomData(f.key(), f.arguments.contentToString())
|
||||
}
|
||||
|
||||
override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
|
||||
super.onFragmentDetached(fm, f)
|
||||
ACRA.errorReporter.removeCustomData(f.key())
|
||||
keys.remove(f)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(activity, savedInstanceState)
|
||||
ACRA.errorReporter.putCustomData(activity.key(), activity.intent.extras.contentToString())
|
||||
(activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(this, true)
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
super.onActivityDestroyed(activity)
|
||||
ACRA.errorReporter.removeCustomData(activity.key())
|
||||
keys.remove(activity)
|
||||
}
|
||||
|
||||
private fun Any.key() = keys.getOrPut(this) {
|
||||
"${time()}: ${javaClass.simpleName}"
|
||||
}
|
||||
|
||||
private fun time() = timeFormat.format(Date())
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun Bundle?.contentToString() = this?.keySet()?.joinToString { k ->
|
||||
val v = get(k)
|
||||
"$k=$v"
|
||||
} ?: toString()
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import android.app.Activity
|
||||
class TaggedActivityResult(
|
||||
val tag: String,
|
||||
val result: Int,
|
||||
)
|
||||
) {
|
||||
|
||||
val TaggedActivityResult.isSuccess: Boolean
|
||||
get() = this.result == Activity.RESULT_OK
|
||||
val isSuccess: Boolean
|
||||
get() = result == Activity.RESULT_OK
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ class WorkManagerHelper(
|
||||
return workManagerImpl.getWorkInfoById(id).await()
|
||||
}
|
||||
|
||||
suspend fun getUniqueWorkInfoByName(name: String): List<WorkInfo> {
|
||||
return workManagerImpl.getWorkInfosForUniqueWork(name).await().orEmpty()
|
||||
}
|
||||
|
||||
suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult {
|
||||
return workManagerImpl.updateWork(request).await()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.database.SQLException
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
@@ -43,7 +44,6 @@ import org.jsoup.internal.StringUtil.StringJoiner
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import kotlin.math.roundToLong
|
||||
@@ -148,13 +148,17 @@ val Context.ramAvailable: Long
|
||||
return result.availMem
|
||||
}
|
||||
|
||||
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.width,
|
||||
view.height,
|
||||
)
|
||||
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
|
||||
ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
0,
|
||||
view.width,
|
||||
view.height,
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun Resources.getLocalesConfig(): LocaleListCompat {
|
||||
val tagsList = StringJoiner(",")
|
||||
|
||||
@@ -26,6 +26,8 @@ fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
|
||||
|
||||
fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
|
||||
|
||||
fun File.isNotEmpty() = length() != 0L
|
||||
|
||||
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,13 @@ import org.koitharu.kotatsu.core.util.Event
|
||||
fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
||||
val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT
|
||||
owner.lifecycleScope.launch(start = start) {
|
||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
collect(collector)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.observe(owner: LifecycleOwner, minState: Lifecycle.State, collector: FlowCollector<T>) {
|
||||
owner.lifecycleScope.launch {
|
||||
owner.lifecycle.repeatOnLifecycle(minState) {
|
||||
collect(collector)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +52,11 @@ suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
|
||||
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
|
||||
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
|
||||
return
|
||||
}
|
||||
show(fm, tag)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.util.UUID
|
||||
|
||||
inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String {
|
||||
inline fun <C : CharSequence> C?.ifNullOrEmpty(defaultValue: () -> C): C {
|
||||
return if (this.isNullOrEmpty()) defaultValue() else this
|
||||
}
|
||||
|
||||
@@ -35,3 +34,9 @@ fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float)
|
||||
val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f)
|
||||
return diff < threshold
|
||||
}
|
||||
|
||||
fun CharSequence.sanitize(): CharSequence {
|
||||
return filterNot { c -> c.isReplacement() }
|
||||
}
|
||||
|
||||
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF'
|
||||
|
||||
@@ -87,3 +87,6 @@ fun Throwable.isWebViewUnavailable(): Boolean {
|
||||
return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
|
||||
cause?.isWebViewUnavailable() == true
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.databinding.ItemTipBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ButtonTip(
|
||||
private val root: ViewGroup,
|
||||
private val insetsDelegate: WindowInsetsDelegate,
|
||||
private val viewModel: DetailsViewModel,
|
||||
) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false)
|
||||
private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize)
|
||||
|
||||
init {
|
||||
selfBinding.textView.setText(R.string.details_button_tip)
|
||||
selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap)
|
||||
selfBinding.root.id = R.id.layout_tip
|
||||
selfBinding.buttonClose.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
remove()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
if (root is CoordinatorLayout) {
|
||||
selfBinding.root.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToRoot() {
|
||||
val lp: ViewGroup.LayoutParams = when (root) {
|
||||
is CoordinatorLayout -> CoordinatorLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
// anchorId = R.id.layout_bottom
|
||||
// anchorGravity = Gravity.TOP
|
||||
gravity = Gravity.BOTTOM
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
bottomMargin += actionBarSize
|
||||
}
|
||||
|
||||
is ConstraintLayout -> ConstraintLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width)
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
}
|
||||
|
||||
else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
root.addView(selfBinding.root, lp)
|
||||
if (root is ConstraintLayout) {
|
||||
val cs = ConstraintSet()
|
||||
cs.clone(root)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END)
|
||||
cs.applyTo(root)
|
||||
}
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
if (root.context.isAnimationsEnabled) {
|
||||
TransitionManager.beginDelayedTransition(root)
|
||||
}
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
root.removeView(selfBinding.root)
|
||||
viewModel.onButtonTipClosed()
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -79,12 +79,11 @@ class ChaptersFragment :
|
||||
return
|
||||
}
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context = view.context,
|
||||
manga = viewModel.manga.value ?: return,
|
||||
state = ReaderState(item.chapter.id, 0, 0),
|
||||
),
|
||||
scaleUpActivityOptionsOf(view).toBundle(),
|
||||
IntentBuilder(view.context)
|
||||
.manga(viewModel.manga.value ?: return)
|
||||
.state(ReaderState(item.chapter.id, 0, 0))
|
||||
.build(),
|
||||
scaleUpActivityOptionsOf(view),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
|
||||
@@ -53,8 +53,9 @@ import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -67,9 +68,10 @@ class DetailsActivity :
|
||||
PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
private lateinit var viewBadge: ViewBadge
|
||||
private var buttonTip: WeakReference<ButtonTip>? = null
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
|
||||
@@ -122,6 +124,7 @@ class DetailsActivity :
|
||||
viewModel.onShowToast.observeEvent(this) {
|
||||
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
viewModel.onShowTip.observeEvent(this) { showTip() }
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.toolbarChapters?.subtitle = it
|
||||
@@ -144,7 +147,7 @@ class DetailsActivity :
|
||||
activity = this,
|
||||
viewModel = viewModel,
|
||||
snackbarHost = viewBinding.containerChapters,
|
||||
shortcutsUpdater = shortcutsUpdater,
|
||||
appShortcutManager = appShortcutManager,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -162,6 +165,8 @@ class DetailsActivity :
|
||||
|
||||
override fun onLongClick(v: View): Boolean = when (v.id) {
|
||||
R.id.button_read -> {
|
||||
buttonTip?.get()?.remove()
|
||||
buttonTip = null
|
||||
val menu = PopupMenu(v.context, v)
|
||||
menu.inflate(R.menu.popup_read)
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
@@ -244,7 +249,7 @@ class DetailsActivity :
|
||||
bottomMargin = insets.bottom + marginEnd
|
||||
}
|
||||
viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
bottomMargin = insets.top
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,12 +301,11 @@ class DetailsActivity :
|
||||
snackbar.show()
|
||||
} else {
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context = this,
|
||||
manga = manga,
|
||||
branch = viewModel.selectedBranchValue,
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
),
|
||||
IntentBuilder(this)
|
||||
.manga(manga)
|
||||
.branch(viewModel.selectedBranchValue)
|
||||
.incognito(isIncognitoMode)
|
||||
.build(),
|
||||
)
|
||||
if (isIncognitoMode) {
|
||||
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
@@ -345,8 +349,16 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTip() {
|
||||
val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel)
|
||||
tip.addToRoot()
|
||||
buttonTip = WeakReference(tip)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TIP_BUTTON = "btn_read"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.model.countChaptersByBranch
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
@@ -45,6 +44,7 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -67,7 +67,7 @@ class DetailsFragment :
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var tagHighlighter: MangaTagHighlighter
|
||||
lateinit var tagHighlighter: ListExtraProvider
|
||||
|
||||
private val viewModel by activityViewModels<DetailsViewModel>()
|
||||
|
||||
@@ -96,8 +96,8 @@ class DetailsFragment :
|
||||
|
||||
override fun onItemClick(item: Bookmark, view: View) {
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(view.context, item),
|
||||
scaleUpActivityOptionsOf(view).toBundle(),
|
||||
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
|
||||
scaleUpActivityOptionsOf(view),
|
||||
)
|
||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -257,7 +257,7 @@ class DetailsFragment :
|
||||
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||
manga.source,
|
||||
),
|
||||
scaleUpActivityOptionsOf(v).toBundle(),
|
||||
scaleUpActivityOptionsOf(v),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,7 @@ class DetailsFragment :
|
||||
manga.tags.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTint(tag),
|
||||
tint = tagHighlighter.getTagTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
|
||||
@@ -29,7 +29,7 @@ class DetailsMenuProvider(
|
||||
private val activity: FragmentActivity,
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val snackbarHost: View,
|
||||
private val shortcutsUpdater: ShortcutsUpdater,
|
||||
private val appShortcutManager: AppShortcutManager,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
@@ -112,7 +112,7 @@ class DetailsMenuProvider(
|
||||
R.id.action_shortcut -> {
|
||||
viewModel.manga.value?.let {
|
||||
activity.lifecycleScope.launch {
|
||||
if (!shortcutsUpdater.requestPinShortcut(it)) {
|
||||
if (!appShortcutManager.requestPinShortcut(it)) {
|
||||
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -37,6 +39,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
||||
import org.koitharu.kotatsu.details.domain.DetailsInteractor
|
||||
@@ -77,6 +80,7 @@ class DetailsViewModel @Inject constructor(
|
||||
private var loadingJob: Job
|
||||
|
||||
val onShowToast = MutableEventFlow<Int>()
|
||||
val onShowTip = MutableEventFlow<Unit>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val manga = doubleManga.map { it?.any }
|
||||
@@ -135,7 +139,7 @@ class DetailsViewModel @Inject constructor(
|
||||
if (description.isNullOrEmpty()) {
|
||||
emit(null)
|
||||
} else {
|
||||
emit(description.parseAsHtml().filterSpans())
|
||||
emit(description.parseAsHtml().filterSpans().sanitize())
|
||||
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
|
||||
@@ -190,6 +194,12 @@ class DetailsViewModel @Inject constructor(
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) {
|
||||
manga.filterNot { it?.chapters.isNullOrEmpty() }.first()
|
||||
onShowTip.call(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
@@ -276,6 +286,10 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onButtonTipClosed() {
|
||||
settings.closeTip(DetailsActivity.TIP_BUTTON)
|
||||
}
|
||||
|
||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||
val result = doubleMangaLoadUseCase(intent)
|
||||
val manga = result.requireAny()
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.sanitize
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
||||
@@ -109,7 +110,7 @@ class ScrobblingInfoSheet :
|
||||
R.id.imageView_cover -> {
|
||||
val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return
|
||||
val options = scaleUpActivityOptionsOf(v)
|
||||
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
|
||||
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +123,7 @@ class ScrobblingInfoSheet :
|
||||
}
|
||||
requireViewBinding().textViewTitle.text = scrobbling.title
|
||||
requireViewBinding().ratingBar.rating = scrobbling.rating * requireViewBinding().ratingBar.numStars
|
||||
requireViewBinding().textViewDescription.text = scrobbling.description
|
||||
requireViewBinding().textViewDescription.text = scrobbling.description?.sanitize()
|
||||
requireViewBinding().spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
|
||||
requireViewBinding().imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
|
||||
requireViewBinding().imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class DownloadsMenuProvider(
|
||||
private val context: Context,
|
||||
@@ -23,6 +24,10 @@ class DownloadsMenuProvider(
|
||||
R.id.action_resume -> viewModel.resumeAll()
|
||||
R.id.action_cancel_all -> confirmCancelAll()
|
||||
R.id.action_remove_completed -> confirmRemoveCompleted()
|
||||
R.id.action_settings -> {
|
||||
context.startActivity(SettingsActivity.newDownloadsSettingsIntent(context))
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.util.UUID
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExploreRepository @Inject constructor(
|
||||
|
||||
@@ -112,7 +112,7 @@ class FavouriteCategoriesActivity :
|
||||
}
|
||||
val intent = FavouritesActivity.newIntent(this, item)
|
||||
val options = scaleUpActivityOptionsOf(view)
|
||||
startActivity(intent, options.toBundle())
|
||||
startActivity(intent, options)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
||||
@@ -81,6 +82,6 @@ class FavouriteCategoriesSheet :
|
||||
)
|
||||
},
|
||||
)
|
||||
}.show(fm, TAG)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
@@ -25,7 +26,7 @@ class MangaCategoriesViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = requireNotNull(savedStateHandle.get<List<ParcelableManga>>(KEY_MANGA_LIST)).map { it.manga }
|
||||
private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga }
|
||||
private val header = CategoriesHeaderItem()
|
||||
|
||||
val content: StateFlow<List<ListModel>> = combine(
|
||||
|
||||
@@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
@@ -21,8 +20,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
|
||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
@@ -30,19 +27,16 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FavouritesListViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val repository: FavouritesRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val tagHighlighter: MangaTagHighlighter,
|
||||
private val listExtraProvider: ListExtraProvider,
|
||||
settings: AppSettings,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
|
||||
) : MangaListViewModel(settings, downloadScheduler) {
|
||||
|
||||
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
||||
|
||||
@@ -76,7 +70,7 @@ class FavouritesListViewModel @Inject constructor(
|
||||
),
|
||||
)
|
||||
|
||||
else -> list.toUi(mode, this, tagHighlighter)
|
||||
else -> list.toUi(mode, listExtraProvider)
|
||||
}
|
||||
}.catch {
|
||||
emit(listOf(it.toErrorState(canRetry = false)))
|
||||
@@ -108,20 +102,4 @@ class FavouritesListViewModel @Inject constructor(
|
||||
repository.setCategoryOrder(categoryId, order)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
return if (settings.isReadingIndicatorsEnabled) {
|
||||
historyRepository.getProgress(mangaId)
|
||||
} else {
|
||||
PROGRESS_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.text.Collator
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
@@ -54,6 +55,6 @@ class FilterSheetFragment :
|
||||
|
||||
private const val TAG = "FilterBottomSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG)
|
||||
fun show(fm: FragmentManager) = FilterSheetFragment().showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.koitharu.kotatsu.history.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
@@ -20,6 +24,10 @@ abstract class HistoryDao {
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
|
||||
abstract fun observeAll(): Flow<List<HistoryWithManga>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
|
||||
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
||||
|
||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@ class HistoryRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAll(limit: Int): Flow<List<Manga>> {
|
||||
return db.historyDao.observeAll(limit).mapItems {
|
||||
it.manga.toManga(it.tags.toMangaTags())
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAllWithHistory(): Flow<List<MangaWithHistory>> {
|
||||
return db.historyDao.observeAll().mapItems {
|
||||
MangaWithHistory(
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import javax.inject.Inject
|
||||
|
||||
class HistoryUpdateUseCase @Inject constructor(
|
||||
|
||||
@@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
@@ -21,8 +20,8 @@ import org.koitharu.kotatsu.core.util.ext.daysDiff
|
||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -31,7 +30,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
@@ -40,8 +38,7 @@ import javax.inject.Inject
|
||||
class HistoryListViewModel @Inject constructor(
|
||||
private val repository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val tagHighlighter: MangaTagHighlighter,
|
||||
private val extraProvider: ListExtraProvider,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler) {
|
||||
|
||||
@@ -106,7 +103,6 @@ class HistoryListViewModel @Inject constructor(
|
||||
mode: ListMode,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
|
||||
val showPercent = settings.isReadingIndicatorsEnabled
|
||||
var prevDate: DateTimeAgo? = null
|
||||
for ((manga, history) in list) {
|
||||
if (grouped) {
|
||||
@@ -116,16 +112,10 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
prevDate = date
|
||||
}
|
||||
val counter = if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(manga.id)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
||||
result += when (mode) {
|
||||
ListMode.LIST -> manga.toListModel(counter, percent)
|
||||
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent, tagHighlighter)
|
||||
ListMode.GRID -> manga.toGridModel(counter, percent)
|
||||
ListMode.LIST -> manga.toListModel(extraProvider)
|
||||
ListMode.DETAILED_LIST -> manga.toListDetailedModel(extraProvider)
|
||||
ListMode.GRID -> manga.toGridModel(extraProvider)
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -1,8 +1,60 @@
|
||||
package org.koitharu.kotatsu.list.domain
|
||||
|
||||
interface ListExtraProvider {
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorRes
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
suspend fun getCounter(mangaId: Long): Int
|
||||
@Reusable
|
||||
class ListExtraProvider @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
private val settings: AppSettings,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun getProgress(mangaId: Long): Float
|
||||
}
|
||||
private val dict by lazy {
|
||||
context.resources.openRawResource(R.raw.tags_redlist).use {
|
||||
val set = HashSet<String>()
|
||||
it.bufferedReader().forEachLine { x ->
|
||||
val line = x.trim()
|
||||
if (line.isNotEmpty()) {
|
||||
set.add(line)
|
||||
}
|
||||
}
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCounter(mangaId: Long): Int {
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProgress(mangaId: Long): Float {
|
||||
return if (settings.isReadingIndicatorsEnabled) {
|
||||
historyRepository.getProgress(mangaId)
|
||||
} else {
|
||||
PROGRESS_NONE
|
||||
}
|
||||
}
|
||||
|
||||
@ColorRes
|
||||
fun getTagTint(tag: MangaTag): Int {
|
||||
return if (tag.title.lowercase() in dict) {
|
||||
R.color.warning
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.domain
|
||||
|
||||
import dagger.Reusable
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class ListExtraProviderImpl @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) : ListExtraProvider {
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
return if (settings.isReadingIndicatorsEnabled) {
|
||||
historyRepository.getProgress(mangaId)
|
||||
} else {
|
||||
PROGRESS_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
||||
import javax.inject.Inject
|
||||
@@ -72,6 +73,6 @@ class ListModeBottomSheet :
|
||||
|
||||
private const val TAG = "ListModeSelectDialog"
|
||||
|
||||
fun show(fm: FragmentManager) = ListModeBottomSheet().show(fm, TAG)
|
||||
fun show(fm: FragmentManager) = ListModeBottomSheet().showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
@@ -56,7 +55,7 @@ import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -114,8 +113,6 @@ abstract class MangaListFragment :
|
||||
fastScroller.setFastScrollListener(this@MangaListFragment)
|
||||
}
|
||||
with(binding.swipeRefreshLayout) {
|
||||
setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary))
|
||||
setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary))
|
||||
setOnRefreshListener(this@MangaListFragment)
|
||||
isEnabled = isSwipeRefreshEnabled
|
||||
}
|
||||
@@ -151,8 +148,8 @@ abstract class MangaListFragment :
|
||||
|
||||
override fun onReadClick(manga: Manga, view: View) {
|
||||
if (selectionController?.onItemClick(manga.id) != true) {
|
||||
val intent = ReaderActivity.newIntent(context ?: return, manga)
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||
val intent = IntentBuilder(view.context).manga(manga).build()
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.model
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.ifZero
|
||||
@@ -14,34 +13,31 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
fun Manga.toListModel(
|
||||
counter: Int,
|
||||
progress: Float,
|
||||
suspend fun Manga.toListModel(
|
||||
extraProvider: ListExtraProvider?
|
||||
) = MangaListModel(
|
||||
id = id,
|
||||
title = title,
|
||||
subtitle = tags.joinToString(", ") { it.title },
|
||||
coverUrl = coverUrl,
|
||||
manga = this,
|
||||
counter = counter,
|
||||
progress = progress,
|
||||
counter = extraProvider?.getCounter(id) ?: 0,
|
||||
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
|
||||
)
|
||||
|
||||
fun Manga.toListDetailedModel(
|
||||
counter: Int,
|
||||
progress: Float,
|
||||
tagHighlighter: MangaTagHighlighter?,
|
||||
suspend fun Manga.toListDetailedModel(
|
||||
extraProvider: ListExtraProvider?,
|
||||
) = MangaListDetailedModel(
|
||||
id = id,
|
||||
title = title,
|
||||
subtitle = altTitle,
|
||||
coverUrl = coverUrl,
|
||||
manga = this,
|
||||
counter = counter,
|
||||
progress = progress,
|
||||
counter = extraProvider?.getCounter(id) ?: 0,
|
||||
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
|
||||
tags = tags.map {
|
||||
ChipsView.ChipModel(
|
||||
tint = tagHighlighter?.getTint(it) ?: 0,
|
||||
tint = extraProvider?.getTagTint(it) ?: 0,
|
||||
title = it.title,
|
||||
icon = 0,
|
||||
isCheckable = false,
|
||||
@@ -51,53 +47,30 @@ fun Manga.toListDetailedModel(
|
||||
},
|
||||
)
|
||||
|
||||
fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel(
|
||||
suspend fun Manga.toGridModel(
|
||||
extraProvider: ListExtraProvider?,
|
||||
) = MangaGridModel(
|
||||
id = id,
|
||||
title = title,
|
||||
coverUrl = coverUrl,
|
||||
manga = this,
|
||||
counter = counter,
|
||||
progress = progress,
|
||||
counter = extraProvider?.getCounter(id) ?: 0,
|
||||
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
|
||||
)
|
||||
|
||||
suspend fun List<Manga>.toUi(
|
||||
mode: ListMode,
|
||||
extraProvider: ListExtraProvider,
|
||||
tagHighlighter: MangaTagHighlighter?,
|
||||
): List<MangaItemModel> = toUi(ArrayList(size), mode, extraProvider, tagHighlighter)
|
||||
|
||||
fun List<Manga>.toUi(
|
||||
mode: ListMode,
|
||||
tagHighlighter: MangaTagHighlighter?,
|
||||
): List<MangaItemModel> = toUi(ArrayList(size), mode, tagHighlighter)
|
||||
|
||||
fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
|
||||
destination: C,
|
||||
mode: ListMode,
|
||||
tagHighlighter: MangaTagHighlighter?,
|
||||
): C = when (mode) {
|
||||
ListMode.LIST -> mapTo(destination) { it.toListModel(0, PROGRESS_NONE) }
|
||||
ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0, PROGRESS_NONE, tagHighlighter) }
|
||||
ListMode.GRID -> mapTo(destination) { it.toGridModel(0, PROGRESS_NONE) }
|
||||
}
|
||||
): List<MangaItemModel> = toUi(ArrayList(size), mode, extraProvider)
|
||||
|
||||
suspend fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
|
||||
destination: C,
|
||||
mode: ListMode,
|
||||
extraProvider: ListExtraProvider,
|
||||
tagHighlighter: MangaTagHighlighter?,
|
||||
): C = when (mode) {
|
||||
ListMode.LIST -> mapTo(destination) {
|
||||
it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
||||
}
|
||||
|
||||
ListMode.DETAILED_LIST -> mapTo(destination) {
|
||||
it.toListDetailedModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id), tagHighlighter)
|
||||
}
|
||||
|
||||
ListMode.GRID -> mapTo(destination) {
|
||||
it.toGridModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
||||
}
|
||||
ListMode.LIST -> mapTo(destination) { it.toListModel(extraProvider) }
|
||||
ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(extraProvider) }
|
||||
ListMode.GRID -> mapTo(destination) { it.toGridModel(extraProvider) }
|
||||
}
|
||||
|
||||
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -33,7 +34,8 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
private val lruCache = SuspendLazy {
|
||||
val dir = cacheDir.get()
|
||||
val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
||||
val availableSize = (getAvailableSize() * 0.8).toLong()
|
||||
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
|
||||
runCatchingCancellable {
|
||||
DiskLruCache.create(dir, size)
|
||||
}.recoverCatching { error ->
|
||||
@@ -63,4 +65,20 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAvailableSize(): Long = runCatchingCancellable {
|
||||
val statFs = StatFs(cacheDir.get().absolutePath)
|
||||
statFs.availableBytes
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(SIZE_DEFAULT)
|
||||
|
||||
private companion object {
|
||||
|
||||
val SIZE_MIN
|
||||
get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES)
|
||||
|
||||
val SIZE_DEFAULT
|
||||
get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.databinding.DialogImportBinding
|
||||
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
|
||||
|
||||
class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener {
|
||||
@@ -64,9 +63,10 @@ class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.On
|
||||
}
|
||||
|
||||
private fun restoreBackup(uri: Uri?) {
|
||||
RestoreDialogFragment.newInstance(uri ?: return)
|
||||
.show(parentFragmentManager, BackupDialogFragment.TAG)
|
||||
dismiss()
|
||||
if (uri != null) {
|
||||
RestoreDialogFragment.show(parentFragmentManager, uri)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.core.net.toUri
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
@@ -17,12 +18,17 @@ import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.filter.ui.FilterOwner
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
|
||||
class LocalListFragment : MangaListFragment() {
|
||||
class LocalListFragment : MangaListFragment(), FilterOwner {
|
||||
|
||||
override val viewModel by viewModels<LocalListViewModel>()
|
||||
|
||||
@@ -65,6 +71,24 @@ class LocalListFragment : MangaListFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override val filterItems: StateFlow<List<ListModel>>
|
||||
get() = viewModel.filterItems
|
||||
|
||||
override val header: StateFlow<FilterHeaderModel>
|
||||
get() = viewModel.header
|
||||
|
||||
override fun applyFilter(tags: Set<MangaTag>) {
|
||||
viewModel.applyFilter(tags)
|
||||
}
|
||||
|
||||
override fun onSortItemClick(item: FilterItem.Sort) {
|
||||
viewModel.onSortItemClick(item)
|
||||
}
|
||||
|
||||
override fun onTagItemClick(item: FilterItem.Tag) {
|
||||
viewModel.onTagItemClick(item)
|
||||
}
|
||||
|
||||
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.delete_manga)
|
||||
|
||||
@@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
@@ -25,7 +24,6 @@ class LocalListViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
filter: FilterCoordinator,
|
||||
tagHighlighter: MangaTagHighlighter,
|
||||
settings: AppSettings,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
listExtraProvider: ListExtraProvider,
|
||||
@@ -35,7 +33,6 @@ class LocalListViewModel @Inject constructor(
|
||||
savedStateHandle,
|
||||
mangaRepositoryFactory,
|
||||
filter,
|
||||
tagHighlighter,
|
||||
settings,
|
||||
listExtraProvider,
|
||||
downloadScheduler,
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
@@ -33,7 +34,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
|
||||
|
||||
private const val TAG = "cleanup"
|
||||
|
||||
fun enqueue(context: Context) {
|
||||
suspend fun enqueue(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
@@ -42,7 +43,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
|
||||
.addTag(TAG)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request).await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
|
||||
@@ -69,8 +69,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -126,8 +124,10 @@ class MainActivity :
|
||||
viewBinding.navRail?.headerView?.setOnClickListener(this)
|
||||
viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
|
||||
|
||||
navigationDelegate =
|
||||
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
|
||||
navigationDelegate = MainNavigationDelegate(
|
||||
navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
|
||||
fragmentManager = supportFragmentManager,
|
||||
)
|
||||
navigationDelegate.addOnFragmentChangedListener(this)
|
||||
navigationDelegate.onCreate()
|
||||
|
||||
@@ -156,6 +156,8 @@ class MainActivity :
|
||||
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
||||
adjustFabVisibility(topFragment = fragment)
|
||||
if (fromUser) {
|
||||
actionModeDelegate.finishActionMode()
|
||||
closeSearchCallback.handleOnBackPressed()
|
||||
viewBinding.appbar.setExpanded(true)
|
||||
}
|
||||
}
|
||||
@@ -243,21 +245,21 @@ class MainActivity :
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
super.onSupportActionModeStarted(mode)
|
||||
adjustFabVisibility()
|
||||
showNav(false)
|
||||
bottomNav?.hide()
|
||||
}
|
||||
|
||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||
super.onSupportActionModeFinished(mode)
|
||||
adjustFabVisibility()
|
||||
showNav(true)
|
||||
bottomNav?.show()
|
||||
}
|
||||
|
||||
private fun onOpenReader(manga: Manga) {
|
||||
val fab = viewBinding.fab ?: viewBinding.navRail?.headerView
|
||||
val options = fab?.let {
|
||||
scaleUpActivityOptionsOf(it).toBundle()
|
||||
scaleUpActivityOptionsOf(it)
|
||||
}
|
||||
startActivity(ReaderActivity.newIntent(this, manga), options)
|
||||
startActivity(IntentBuilder(this).manga(manga).build(), options)
|
||||
}
|
||||
|
||||
private fun onCountersChanged(counters: SparseIntArray) {
|
||||
@@ -301,17 +303,6 @@ class MainActivity :
|
||||
closeSearchCallback.isEnabled = false
|
||||
}
|
||||
|
||||
private fun showNav(visible: Boolean) {
|
||||
bottomNav?.run {
|
||||
if (visible) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
viewBinding.navRail?.isVisible = visible
|
||||
}
|
||||
|
||||
private fun isSearchOpened(): Boolean {
|
||||
return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null
|
||||
}
|
||||
@@ -328,8 +319,6 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Default) {
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
LocalStorageCleanupWorker.enqueue(applicationContext)
|
||||
}
|
||||
withResumed {
|
||||
@@ -340,7 +329,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun adjustFabVisibility(
|
||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
|
||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value,
|
||||
topFragment: Fragment? = navigationDelegate.primaryFragment,
|
||||
isSearchOpened: Boolean = isSearchOpened(),
|
||||
) {
|
||||
@@ -383,7 +372,7 @@ class MainActivity :
|
||||
supportActionBar?.setHomeAsUpIndicator(
|
||||
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
|
||||
)
|
||||
showNav(!isOpened)
|
||||
bottomNav?.showOrHide(!isOpened)
|
||||
}
|
||||
|
||||
private fun requestNotificationsPermission() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -12,6 +12,7 @@ import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -32,6 +33,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||
@@ -40,7 +42,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -103,7 +105,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
|
||||
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
|
||||
var task = tasks[page.id]
|
||||
var task = tasks[page.id]?.takeIf { it.isValid() }
|
||||
if (force) {
|
||||
task?.cancel()
|
||||
} else if (task?.isCancelled == false) {
|
||||
@@ -216,6 +218,15 @@ class PageLoader @Inject constructor(
|
||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||
}
|
||||
|
||||
private fun Deferred<File>.isValid(): Boolean {
|
||||
return if (isCompleted) {
|
||||
val file = getCompleted()
|
||||
file.exists() && file.isNotEmpty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||
CoroutineExceptionHandler {
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
@@ -87,6 +88,6 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(), OnListItemClick
|
||||
) = ChaptersSheet().withArgs(2) {
|
||||
putParcelable(ARG_CHAPTERS, ParcelableMangaChapters(chapters))
|
||||
putLong(ARG_CURRENT_ID, currentId)
|
||||
}.show(fm, TAG)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -127,7 +128,7 @@ class ReaderActivity :
|
||||
},
|
||||
),
|
||||
)
|
||||
viewModel.readerMode.observe(this, this::onInitReader)
|
||||
viewModel.readerMode.observe(this, Lifecycle.State.STARTED, this::onInitReader)
|
||||
viewModel.onPageSaved.observeEvent(this, this::onPageSaved)
|
||||
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
@@ -398,6 +399,44 @@ class ReaderActivity :
|
||||
}
|
||||
}
|
||||
|
||||
class IntentBuilder(context: Context) {
|
||||
|
||||
private val intent = Intent(context, ReaderActivity::class.java)
|
||||
.setAction(ACTION_MANGA_READ)
|
||||
|
||||
fun manga(manga: Manga) = apply {
|
||||
intent.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
}
|
||||
|
||||
fun mangaId(mangaId: Long) = apply {
|
||||
intent.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
|
||||
fun incognito(incognito: Boolean) = apply {
|
||||
intent.putExtra(EXTRA_INCOGNITO, incognito)
|
||||
}
|
||||
|
||||
fun branch(branch: String?) = apply {
|
||||
intent.putExtra(EXTRA_BRANCH, branch)
|
||||
}
|
||||
|
||||
fun state(state: ReaderState?) = apply {
|
||||
intent.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
fun bookmark(bookmark: Bookmark) = manga(
|
||||
bookmark.manga,
|
||||
).state(
|
||||
ReaderState(
|
||||
chapterId = bookmark.chapterId,
|
||||
page = bookmark.page,
|
||||
scroll = bookmark.scroll,
|
||||
),
|
||||
)
|
||||
|
||||
fun build() = intent
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
|
||||
@@ -405,38 +444,5 @@ class ReaderActivity :
|
||||
const val EXTRA_BRANCH = "branch"
|
||||
const val EXTRA_INCOGNITO = "incognito"
|
||||
private const val TOAST_DURATION = 1500L
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, branch: String?, isIncognitoMode: Boolean): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
.putExtra(EXTRA_BRANCH, branch)
|
||||
.putExtra(EXTRA_INCOGNITO, isIncognitoMode)
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
.putExtra(EXTRA_STATE, state)
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, bookmark: Bookmark): Intent {
|
||||
val state = ReaderState(
|
||||
chapterId = bookmark.chapterId,
|
||||
page = bookmark.page,
|
||||
scroll = bookmark.scroll,
|
||||
)
|
||||
return newIntent(context, bookmark.manga, state)
|
||||
.putExtra(EXTRA_INCOGNITO, true)
|
||||
}
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
||||
return Intent(context, ReaderActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.koitharu.kotatsu.core.util.ext.measureDimension
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -30,7 +30,7 @@ import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -42,6 +42,7 @@ 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.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
|
||||
import org.koitharu.kotatsu.details.domain.model.DoubleManga
|
||||
@@ -56,7 +57,6 @@ import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -73,7 +73,7 @@ class ReaderViewModel @Inject constructor(
|
||||
private val pageSaveHelper: PageSaveHelper,
|
||||
private val pageLoader: PageLoader,
|
||||
private val chaptersLoader: ChaptersLoader,
|
||||
private val shortcutsUpdater: ShortcutsUpdater,
|
||||
private val appShortcutManager: AppShortcutManager,
|
||||
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
|
||||
private val historyUpdateUseCase: HistoryUpdateUseCase,
|
||||
private val detectReaderModeUseCase: DetectReaderModeUseCase,
|
||||
@@ -155,7 +155,7 @@ class ReaderViewModel @Inject constructor(
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
launchJob(Dispatchers.Default) {
|
||||
val mangaId = mangaFlow.filterNotNull().first().id
|
||||
shortcutsUpdater.notifyMangaOpened(mangaId)
|
||||
appShortcutManager.notifyMangaOpened(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ class ReaderViewModel @Inject constructor(
|
||||
chapterId = state.chapterId,
|
||||
page = state.page,
|
||||
scroll = state.scroll,
|
||||
imageUrl = page.preview.ifNullOrEmpty { pageLoader.getPageUrl(page) },
|
||||
imageUrl = page.preview.ifNullOrEmpty { page.url },
|
||||
createdAt = Date(),
|
||||
percent = computePercent(state.chapterId, state.page),
|
||||
)
|
||||
@@ -312,8 +312,10 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
private fun loadImpl() {
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
var manga =
|
||||
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", ""))
|
||||
var manga = DoubleManga(
|
||||
dataRepository.resolveIntent(intent)
|
||||
?: throw NotFoundException("Cannot find manga", ""),
|
||||
)
|
||||
mangaData.value = manga
|
||||
manga = doubleMangaLoadUseCase(intent)
|
||||
chaptersLoader.init(manga)
|
||||
|
||||
@@ -68,10 +68,10 @@ class ColorFilterConfigActivity :
|
||||
|
||||
viewModel.colorFilter.observe(this, this::onColorFilterChanged)
|
||||
viewModel.isLoading.observe(this, this::onLoadingChanged)
|
||||
viewModel.preview.observe(this, this::onPreviewChanged)
|
||||
viewModel.onDismiss.observeEvent(this) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
loadPreview(viewModel.preview)
|
||||
}
|
||||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
@@ -114,13 +114,13 @@ class ColorFilterConfigActivity :
|
||||
viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
|
||||
}
|
||||
|
||||
private fun onPreviewChanged(preview: MangaPage?) {
|
||||
if (preview == null) return
|
||||
private fun loadPreview(page: MangaPage) {
|
||||
val data: Any = page.preview?.takeUnless { it.isEmpty() } ?: page
|
||||
ImageRequest.Builder(this@ColorFilterConfigActivity)
|
||||
.data(preview)
|
||||
.data(data)
|
||||
.scale(Scale.FILL)
|
||||
.decodeRegion()
|
||||
.tag(preview.source)
|
||||
.tag(page.source)
|
||||
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
|
||||
.error(R.drawable.ic_error_placeholder)
|
||||
.size(ViewSizeResolver(viewBinding.imageViewBefore))
|
||||
|
||||
@@ -7,11 +7,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
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.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
||||
import javax.inject.Inject
|
||||
@@ -19,38 +18,24 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class ColorFilterConfigViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
|
||||
private val manga = savedStateHandle.require<ParcelableManga>(EXTRA_MANGA).manga
|
||||
|
||||
private var initialColorFilter: ReaderColorFilter? = null
|
||||
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
|
||||
val onDismiss = MutableEventFlow<Unit>()
|
||||
val preview = MutableStateFlow<MangaPage?>(null)
|
||||
val preview = savedStateHandle.require<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES).pages.first()
|
||||
|
||||
val isChanged: Boolean
|
||||
get() = colorFilter.value != initialColorFilter
|
||||
|
||||
init {
|
||||
val page = checkNotNull(
|
||||
savedStateHandle.get<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(),
|
||||
)
|
||||
launchLoadingJob {
|
||||
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
|
||||
colorFilter.value = initialColorFilter
|
||||
}
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val repository = mangaRepositoryFactory.create(page.source)
|
||||
val url = repository.getPageUrl(page)
|
||||
preview.value = MangaPage(
|
||||
id = page.id,
|
||||
url = url,
|
||||
preview = page.preview,
|
||||
source = page.source,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBrightness(brightness: Float) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
|
||||
@@ -192,6 +193,6 @@ class ReaderConfigSheet :
|
||||
|
||||
fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigSheet().withArgs(1) {
|
||||
putInt(ARG_MODE, mode.id)
|
||||
}.show(fm, TAG)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
|
||||
protected val viewModel by activityViewModels<ReaderViewModel>()
|
||||
private var stateToSave: ReaderState? = null
|
||||
|
||||
protected var readerAdapter: BaseReaderAdapter<*>? = null
|
||||
private set
|
||||
|
||||
override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
var restoredState = savedInstanceState?.getParcelableCompat<ReaderState>(KEY_STATE)
|
||||
readerAdapter = onCreateAdapter()
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
onPagesChanged(it.pages, restoredState ?: it.state)
|
||||
@@ -34,6 +38,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
|
||||
|
||||
override fun onDestroyView() {
|
||||
stateToSave = getCurrentState()
|
||||
readerAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -45,6 +50,10 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
|
||||
outState.putParcelable(KEY_STATE, stateToSave)
|
||||
}
|
||||
|
||||
protected fun requireAdapter() = checkNotNull(readerAdapter) {
|
||||
"Adapter was not created or already destroyed"
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
abstract fun switchPageBy(delta: Int)
|
||||
@@ -55,5 +64,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
|
||||
|
||||
abstract fun getCurrentState(): ReaderState?
|
||||
|
||||
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
|
||||
protected abstract fun onCreateAdapter(): BaseReaderAdapter<*>
|
||||
|
||||
protected abstract suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user