Compare commits

..

7 Commits

Author SHA1 Message Date
fd01367601 Move Kitsu scrobbler to kotlin dir 2023-06-05 17:33:12 +03:00
cb64740349 Merge branch 'devel' into feature/kitsu
# Conflicts:
#	app/src/main/res/values/strings.xml
2023-06-05 17:21:56 +03:00
6fdcaf0d02 Merge branch 'devel' into feature/kitsu
# Conflicts:
#	app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
#	app/src/main/res/values/strings.xml
2023-05-29 17:29:05 +03:00
56de725cf1 Open Kitsu auth activity 2023-05-06 18:15:24 +03:00
7a2ad47405 Merge branch 'devel' into feature/kitsu 2023-05-06 18:07:02 +03:00
41551451b0 Part 1 2023-05-06 16:15:17 +03:00
d5c24cd5c8 Initial adding of Kitsu scrobbler 2023-05-03 13:54:19 +03:00
217 changed files with 3746 additions and 4235 deletions

View File

@@ -1,3 +0,0 @@
[weblate]
url = https://hosted.weblate.org/api/
translation = kotatsu/strings

View File

@@ -19,7 +19,7 @@ Kotatsu is a free and open source manga reader for Android.
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password/fingerprint protect access to the app
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 551
versionName '5.2-b1'
versionCode 547
versionName '5.1.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -83,13 +83,13 @@ dependencies {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
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.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
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'

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.core.util
package org.koitharu.kotatsu.util
import android.util.Log
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver

View File

@@ -1,3 +1,3 @@
package org.koitharu.kotatsu.core.util.ext
package org.koitharu.kotatsu.util.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

View File

@@ -102,10 +102,6 @@
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"
@@ -163,9 +159,21 @@
<data android:host="shikimori-auth" />
<data android:host="anilist-auth" />
<data android:host="mal-auth" />
<data android:host="kitsu-auth" />
</intent-filter>
</activity>
<activity
android:name=".scrobbling.kitsu.ui.KitsuAuthActivity"
android:exported="false"
android:label="@string/kitsu"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="kotatsu+kitsu" />
</intent-filter>
</activity>
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service

View File

@@ -1,7 +1,6 @@
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(
@@ -15,20 +14,6 @@ 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

View File

@@ -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

View File

@@ -95,11 +95,8 @@ class BookmarksFragment :
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
val intent = ReaderActivity.IntentBuilder(view.context)
.bookmark(item)
.incognito(true)
.build()
startActivity(intent, scaleUpActivityOptionsOf(view))
val intent = ReaderActivity.newIntent(view.context, item)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -28,8 +28,7 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener)
bind {
val data: Any = item.directImageUrl ?: item.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)

View File

@@ -103,7 +103,6 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onDestroy() {
super.onDestroy()
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}

View File

@@ -1,175 +0,0 @@
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)
}
}
}
}

View File

@@ -0,0 +1,123 @@
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)
}
}
}
}

View File

@@ -40,6 +40,8 @@ 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
@@ -63,6 +65,9 @@ interface AppModule {
@Binds
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
@Binds
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
companion object {
@Provides

View File

@@ -6,13 +6,15 @@ 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.CloudFlareActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
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
@@ -21,26 +23,20 @@ import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
class ExceptionResolver private constructor(
private val activity: FragmentActivity?,
private val fragment: Fragment?,
) : ActivityResultCallback<TaggedActivityResult> {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
private val activity: FragmentActivity?
private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>>
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource>
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.fragment = fragment
activity = null
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this)
}
override fun onActivityResult(result: TaggedActivityResult) {
@@ -62,9 +58,22 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
else -> false
}
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
continuations[CloudFlareActivity.TAG] = cont
cloudflareContract.launch(url to headers)
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 resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->

View File

@@ -16,12 +16,12 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.BaseHttpClient
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

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.network
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.net.IDN
import java.util.Locale
import javax.inject.Inject

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.net.InetAddress
import java.net.UnknownHostException

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.network
import android.annotation.SuppressLint
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext

View File

@@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
private const val PREFS_NAME = "cookies"

View File

@@ -26,12 +26,12 @@ 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.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
@@ -142,9 +142,8 @@ class ShortcutsUpdater @Inject constructor(
.setLongLabel(manga.title)
.setIcon(icon)
.setIntent(
ReaderActivity.IntentBuilder(context)
.mangaId(manga.id)
.build(),
ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ),
)
}

View File

@@ -0,0 +1,37 @@
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
}
}
}

View File

@@ -428,6 +428,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SHIKIMORI = "shikimori"
const val KEY_ANILIST = "anilist"
const val KEY_MAL = "mal"
const val KEY_KITSU = "kitsu"
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
const val KEY_DOWNLOADS_WIFI = "downloads_wifi"
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"

View File

@@ -27,7 +27,6 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
.setView(binding.root)
.run(::onBuildDialog)
.create()
.also(::onDialogCreated)
}
final override fun onCreateView(

View File

@@ -45,7 +45,7 @@ abstract class BaseActivity<B : ViewBinding> :
protected val exceptionResolver = ExceptionResolver(this)
@JvmField
protected val insetsDelegate = WindowInsetsDelegate()
protected val insetsDelegate = WindowInsetsDelegate(this)
@JvmField
val actionModeDelegate = ActionModeDelegate()
@@ -62,7 +62,6 @@ abstract class BaseActivity<B : ViewBinding> :
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true
insetsDelegate.addInsetsListener(this)
putDataToExtras(intent)
}

View File

@@ -26,7 +26,7 @@ abstract class BaseFragment<B : ViewBinding> :
protected val exceptionResolver = ExceptionResolver(this)
@JvmField
protected val insetsDelegate = WindowInsetsDelegate()
protected val insetsDelegate = WindowInsetsDelegate(this)
protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
@@ -44,13 +44,11 @@ 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()
}

View File

@@ -4,11 +4,9 @@ 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>() {
@@ -20,11 +18,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
with(window) {
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
statusBarColor = Color.TRANSPARENT
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
} else {
Color.TRANSPARENT
}
navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

View File

@@ -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.core.util.ext.getThemeColor
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
import javax.inject.Inject
@Suppress("LeakingThis")
@AndroidEntryPoint
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(),
@@ -26,28 +26,27 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
lateinit var settings: AppSettings
@JvmField
protected val insetsDelegate = WindowInsetsDelegate()
protected val insetsDelegate = WindowInsetsDelegate(this)
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()
setTitle(if (titleId != 0) getString(titleId) else null)
if (titleId != 0) {
setTitle(getString(titleId))
}
}
@CallSuper
@@ -57,7 +56,9 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
)
}
protected fun setTitle(title: CharSequence?) {
(activity as? SettingsActivity)?.setSectionTitle(title)
@Suppress("UsePropertyAccessSyntax")
protected fun setTitle(title: CharSequence) {
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title)
}
}

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
abstract class CoroutineIntentService : BaseService() {

View File

@@ -9,7 +9,6 @@ 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.*
@@ -25,7 +24,6 @@ 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
@@ -117,9 +115,6 @@ class FastScroller @JvmOverloads constructor(
return viewHeight * proportion
}
val isScrollbarVisible: Boolean
get() = binding.scrollbar.isVisible
init {
clipChildren = false
orientation = HORIZONTAL
@@ -170,9 +165,7 @@ class FastScroller @JvmOverloads constructor(
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (!isScrollbarVisible || event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) {
return false
}
if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false
requestDisallowInterceptTouchEvent(true)
setHandleSelected(true)
@@ -303,12 +296,10 @@ class FastScroller @JvmOverloads constructor(
if (parent is ViewGroup) {
setLayoutParams(parent as ViewGroup)
} else {
val viewGroup = findValidParent(recyclerView)
if (viewGroup != null) {
viewGroup.addView(this)
setLayoutParams(viewGroup)
}
} else if (recyclerView.parent is ViewGroup) {
val viewGroup = recyclerView.parent as ViewGroup
viewGroup.addView(this)
setLayoutParams(viewGroup)
}
recyclerView.addOnScrollListener(scrollListener)
@@ -522,14 +513,6 @@ 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)

View File

@@ -6,6 +6,7 @@ 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
@@ -35,7 +36,7 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher
get() = requireComponentDialog().onBackPressedDispatcher
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher
final override fun onCreateView(
inflater: LayoutInflater,

View File

@@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
get() = activeActionMode != null
override fun handleOnBackPressed() {
finishActionMode()
activeActionMode?.finish()
}
fun onSupportActionModeStarted(mode: ActionMode) {
@@ -45,10 +45,6 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
}
fun finishActionMode() {
activeActionMode?.finish()
}
private inner class ListenerLifecycleObserver(
private val listener: ActionModeListener,
) : DefaultLifecycleObserver {

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
fun interface ReversibleHandle {

View File

@@ -5,9 +5,10 @@ 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 : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
class WindowInsetsDelegate(
private val listener: WindowInsetsListener,
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
@JvmField
var handleImeInsets: Boolean = false
@@ -15,7 +16,6 @@ class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeLis
@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 : OnApplyWindowInsetsListener, View.OnLayoutChangeLis
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
}
if (newInsets != lastInsets) {
listeners.forEach { it.onWindowInsetsChanged(newInsets) }
listener.onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return handledInsets
@@ -52,15 +52,6 @@ class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeLis
}
}
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)

View File

@@ -92,14 +92,6 @@ 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)

View File

@@ -5,8 +5,7 @@ import android.app.Activity
class TaggedActivityResult(
val tag: String,
val result: Int,
) {
)
val isSuccess: Boolean
get() = result == Activity.RESULT_OK
}
val TaggedActivityResult.isSuccess: Boolean
get() = this.result == Activity.RESULT_OK

View File

@@ -16,7 +16,6 @@ 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
@@ -44,6 +43,7 @@ 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,17 +148,13 @@ val Context.ramAvailable: Long
return result.availMem
}
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.width,
view.height,
).toBundle()
} else {
null
}
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.width,
view.height,
)
fun Resources.getLocalesConfig(): LocaleListCompat {
val tagsList = StringJoiner(",")

View File

@@ -26,8 +26,6 @@ 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()
}

View File

@@ -14,7 +14,9 @@ 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) {
collect(collector)
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect(collector)
}
}
}

View File

@@ -2,9 +2,10 @@ 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 <C : CharSequence> C?.ifNullOrEmpty(defaultValue: () -> C): C {
inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String {
return if (this.isNullOrEmpty()) defaultValue() else this
}
@@ -34,9 +35,3 @@ 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'

View File

@@ -87,6 +87,3 @@ fun Throwable.isWebViewUnavailable(): Boolean {
return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
cause?.isWebViewUnavailable() == true
}
@Suppress("FunctionName")
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
@AndroidEntryPoint

View File

@@ -1,92 +0,0 @@
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()
}
}

View File

@@ -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.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import kotlin.math.roundToInt
@@ -79,11 +79,12 @@ class ChaptersFragment :
return
}
startActivity(
IntentBuilder(view.context)
.manga(viewModel.manga.value ?: return)
.state(ReaderState(item.chapter.id, 0, 0))
.build(),
scaleUpActivityOptionsOf(view),
ReaderActivity.newIntent(
context = view.context,
manga = viewModel.manga.value ?: return,
state = ReaderState(item.chapter.id, 0, 0),
),
scaleUpActivityOptionsOf(view).toBundle(),
)
}

View File

@@ -53,9 +53,8 @@ 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.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderActivity
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
@@ -71,7 +70,6 @@ class DetailsActivity :
lateinit var shortcutsUpdater: ShortcutsUpdater
private lateinit var viewBadge: ViewBadge
private var buttonTip: WeakReference<ButtonTip>? = null
private val viewModel: DetailsViewModel by viewModels()
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
@@ -124,7 +122,6 @@ 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
@@ -165,8 +162,6 @@ 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)
@@ -249,7 +244,7 @@ class DetailsActivity :
bottomMargin = insets.bottom + marginEnd
}
viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.top
bottomMargin = insets.bottom
}
}
@@ -301,11 +296,12 @@ class DetailsActivity :
snackbar.show()
} else {
startActivity(
IntentBuilder(this)
.manga(manga)
.branch(viewModel.selectedBranchValue)
.incognito(isIncognitoMode)
.build(),
ReaderActivity.newIntent(
context = this,
manga = manga,
branch = viewModel.selectedBranchValue,
isIncognitoMode = isIncognitoMode,
),
)
if (isIncognitoMode) {
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
@@ -349,16 +345,8 @@ 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))

View File

@@ -23,6 +23,7 @@ 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
@@ -44,7 +45,6 @@ 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: ListExtraProvider
lateinit var tagHighlighter: MangaTagHighlighter
private val viewModel by activityViewModels<DetailsViewModel>()
@@ -96,8 +96,8 @@ class DetailsFragment :
override fun onItemClick(item: Bookmark, view: View) {
startActivity(
ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
scaleUpActivityOptionsOf(view),
ReaderActivity.newIntent(view.context, item),
scaleUpActivityOptionsOf(view).toBundle(),
)
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),
scaleUpActivityOptionsOf(v).toBundle(),
)
}
}
@@ -283,7 +283,7 @@ class DetailsFragment :
manga.tags.map { tag ->
ChipsView.ChipModel(
title = tag.title,
tint = tagHighlighter.getTagTint(tag),
tint = tagHighlighter.getTint(tag),
icon = 0,
data = tag,
isCheckable = false,

View File

@@ -18,8 +18,6 @@ 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
@@ -39,7 +37,6 @@ 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
@@ -80,7 +77,6 @@ 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 }
@@ -139,7 +135,7 @@ class DetailsViewModel @Inject constructor(
if (description.isNullOrEmpty()) {
emit(null)
} else {
emit(description.parseAsHtml().filterSpans().sanitize())
emit(description.parseAsHtml().filterSpans())
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
@@ -194,12 +190,6 @@ 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() {
@@ -286,10 +276,6 @@ 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()

View File

@@ -23,7 +23,6 @@ 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
@@ -110,7 +109,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)
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
}
}
}
@@ -123,7 +122,7 @@ class ScrobblingInfoSheet :
}
requireViewBinding().textViewTitle.text = scrobbling.title
requireViewBinding().ratingBar.rating = scrobbling.rating * requireViewBinding().ratingBar.numStars
requireViewBinding().textViewDescription.text = scrobbling.description?.sanitize()
requireViewBinding().textViewDescription.text = scrobbling.description
requireViewBinding().spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
requireViewBinding().imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
requireViewBinding().imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)

View File

@@ -7,7 +7,6 @@ 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,
@@ -24,10 +23,6 @@ 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

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.UUID
import com.google.android.material.R as materialR

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
class ExploreRepository @Inject constructor(

View File

@@ -112,7 +112,7 @@ class FavouriteCategoriesActivity :
}
val intent = FavouritesActivity.newIntent(this, item)
val options = scaleUpActivityOptionsOf(view)
startActivity(intent, options)
startActivity(intent, options.toBundle())
}
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {

View File

@@ -12,7 +12,6 @@ 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
@@ -26,7 +25,7 @@ class MangaCategoriesViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga }
private val manga = requireNotNull(savedStateHandle.get<List<ParcelableManga>>(KEY_MANGA_LIST)).map { it.manga }
private val header = CategoriesHeaderItem()
val content: StateFlow<List<ListModel>> = combine(

View File

@@ -13,6 +13,7 @@ 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
@@ -20,6 +21,8 @@ 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
@@ -27,16 +30,19 @@ 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 listExtraProvider: ListExtraProvider,
settings: AppSettings,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
@@ -70,7 +76,7 @@ class FavouritesListViewModel @Inject constructor(
),
)
else -> list.toUi(mode, listExtraProvider)
else -> list.toUi(mode, this, tagHighlighter)
}
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
@@ -102,4 +108,20 @@ 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
}
}
}

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.text.Collator
import java.util.LinkedList
import java.util.Locale

View File

@@ -1,10 +1,6 @@
package org.koitharu.kotatsu.history.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
@@ -24,10 +20,6 @@ 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>

View File

@@ -51,12 +51,6 @@ 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(

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
class HistoryUpdateUseCase @Inject constructor(

View File

@@ -10,6 +10,7 @@ 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
@@ -20,8 +21,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
@@ -30,6 +31,7 @@ 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
@@ -38,7 +40,8 @@ import javax.inject.Inject
class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository,
private val settings: AppSettings,
private val extraProvider: ListExtraProvider,
private val trackingRepository: TrackingRepository,
private val tagHighlighter: MangaTagHighlighter,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
@@ -103,6 +106,7 @@ 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) {
@@ -112,10 +116,16 @@ 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(extraProvider)
ListMode.DETAILED_LIST -> manga.toListDetailedModel(extraProvider)
ListMode.GRID -> manga.toGridModel(extraProvider)
ListMode.LIST -> manga.toListModel(counter, percent)
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent, tagHighlighter)
ListMode.GRID -> manga.toGridModel(counter, percent)
}
}
return result

View File

@@ -1,60 +1,8 @@
package org.koitharu.kotatsu.list.domain
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
interface ListExtraProvider {
@Reusable
class ListExtraProvider @Inject constructor(
@ApplicationContext context: Context,
private val settings: AppSettings,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
) {
suspend fun getCounter(mangaId: Long): Int
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
}
}
}
suspend fun getProgress(mangaId: Long): Float
}

View File

@@ -0,0 +1,32 @@
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
}
}
}

View File

@@ -35,6 +35,7 @@ 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
@@ -55,7 +56,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.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
@@ -113,6 +114,8 @@ 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
}
@@ -148,8 +151,8 @@ abstract class MangaListFragment :
override fun onReadClick(manga: Manga, view: View) {
if (selectionController?.onItemClick(manga.id) != true) {
val intent = IntentBuilder(view.context).manga(manga).build()
startActivity(intent, scaleUpActivityOptionsOf(view))
val intent = ReaderActivity.newIntent(context ?: return, manga)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
}
}

View File

@@ -3,6 +3,7 @@ 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
@@ -13,31 +14,34 @@ import org.koitharu.kotatsu.parsers.model.Manga
import java.net.SocketTimeoutException
import java.net.UnknownHostException
suspend fun Manga.toListModel(
extraProvider: ListExtraProvider?
fun Manga.toListModel(
counter: Int,
progress: Float,
) = MangaListModel(
id = id,
title = title,
subtitle = tags.joinToString(", ") { it.title },
coverUrl = coverUrl,
manga = this,
counter = extraProvider?.getCounter(id) ?: 0,
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
counter = counter,
progress = progress,
)
suspend fun Manga.toListDetailedModel(
extraProvider: ListExtraProvider?,
fun Manga.toListDetailedModel(
counter: Int,
progress: Float,
tagHighlighter: MangaTagHighlighter?,
) = MangaListDetailedModel(
id = id,
title = title,
subtitle = altTitle,
coverUrl = coverUrl,
manga = this,
counter = extraProvider?.getCounter(id) ?: 0,
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
counter = counter,
progress = progress,
tags = tags.map {
ChipsView.ChipModel(
tint = extraProvider?.getTagTint(it) ?: 0,
tint = tagHighlighter?.getTint(it) ?: 0,
title = it.title,
icon = 0,
isCheckable = false,
@@ -47,30 +51,53 @@ suspend fun Manga.toListDetailedModel(
},
)
suspend fun Manga.toGridModel(
extraProvider: ListExtraProvider?,
) = MangaGridModel(
fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel(
id = id,
title = title,
coverUrl = coverUrl,
manga = this,
counter = extraProvider?.getCounter(id) ?: 0,
progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
counter = counter,
progress = progress,
)
suspend fun List<Manga>.toUi(
mode: ListMode,
extraProvider: ListExtraProvider,
): List<MangaItemModel> = toUi(ArrayList(size), mode, extraProvider)
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) }
}
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) }
ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(extraProvider) }
ListMode.GRID -> mapTo(destination) { it.toGridModel(extraProvider) }
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))
}
}
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import java.util.EnumSet
import javax.inject.Inject

View File

@@ -1,7 +1,6 @@
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
@@ -18,7 +17,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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@@ -34,8 +33,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
}
private val lruCache = SuspendLazy {
val dir = cacheDir.get()
val availableSize = (getAvailableSize() * 0.8).toLong()
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
runCatchingCancellable {
DiskLruCache.create(dir, size)
}.recoverCatching { error ->
@@ -65,20 +63,4 @@ 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)
}
}

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.IOException
import javax.inject.Inject

View File

@@ -12,6 +12,7 @@ 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 {
@@ -63,10 +64,9 @@ class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.On
}
private fun restoreBackup(uri: Uri?) {
if (uri != null) {
RestoreDialogFragment.show(parentFragmentManager, uri)
dismiss()
}
RestoreDialogFragment.newInstance(uri ?: return)
.show(parentFragmentManager, BackupDialogFragment.TAG)
dismiss()
}
companion object {

View File

@@ -6,6 +6,7 @@ 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
@@ -24,6 +25,7 @@ class LocalListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator,
tagHighlighter: MangaTagHighlighter,
settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
listExtraProvider: ListExtraProvider,
@@ -33,6 +35,7 @@ class LocalListViewModel @Inject constructor(
savedStateHandle,
mangaRepositoryFactory,
filter,
tagHighlighter,
settings,
listExtraProvider,
downloadScheduler,

View File

@@ -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.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
@@ -126,10 +126,8 @@ class MainActivity :
viewBinding.navRail?.headerView?.setOnClickListener(this)
viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
navigationDelegate = MainNavigationDelegate(
navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
fragmentManager = supportFragmentManager,
)
navigationDelegate =
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
navigationDelegate.addOnFragmentChangedListener(this)
navigationDelegate.onCreate()
@@ -158,8 +156,6 @@ class MainActivity :
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
adjustFabVisibility(topFragment = fragment)
if (fromUser) {
actionModeDelegate.finishActionMode()
closeSearchCallback.handleOnBackPressed()
viewBinding.appbar.setExpanded(true)
}
}
@@ -247,21 +243,21 @@ class MainActivity :
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
adjustFabVisibility()
bottomNav?.hide()
showNav(false)
}
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
adjustFabVisibility()
bottomNav?.show()
showNav(true)
}
private fun onOpenReader(manga: Manga) {
val fab = viewBinding.fab ?: viewBinding.navRail?.headerView
val options = fab?.let {
scaleUpActivityOptionsOf(it)
scaleUpActivityOptionsOf(it).toBundle()
}
startActivity(IntentBuilder(this).manga(manga).build(), options)
startActivity(ReaderActivity.newIntent(this, manga), options)
}
private fun onCountersChanged(counters: SparseIntArray) {
@@ -305,6 +301,17 @@ 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
}
@@ -333,7 +340,7 @@ class MainActivity :
}
private fun adjustFabVisibility(
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value,
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
topFragment: Fragment? = navigationDelegate.primaryFragment,
isSearchOpened: Boolean = isSearchOpened(),
) {
@@ -376,7 +383,7 @@ class MainActivity :
supportActionBar?.setHomeAsUpIndicator(
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
)
bottomNav?.showOrHide(!isOpened)
showNav(!isOpened)
}
private fun requestNotificationsPermission() {

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject

View File

@@ -12,7 +12,6 @@ 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
@@ -33,7 +32,6 @@ 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
@@ -42,7 +40,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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger
@@ -105,7 +103,7 @@ class PageLoader @Inject constructor(
}
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
var task = tasks[page.id]?.takeIf { it.isValid() }
var task = tasks[page.id]
if (force) {
task?.cancel()
} else if (task?.isCancelled == false) {
@@ -218,15 +216,6 @@ 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 {

View File

@@ -398,44 +398,6 @@ 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"
@@ -443,5 +405,38 @@ 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)
}
}
}

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.text.SimpleDateFormat
import java.util.Date
import com.google.android.material.R as materialR

View File

@@ -42,7 +42,6 @@ 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
@@ -57,6 +56,7 @@ 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
@@ -288,7 +288,7 @@ class ReaderViewModel @Inject constructor(
chapterId = state.chapterId,
page = state.page,
scroll = state.scroll,
imageUrl = page.preview.ifNullOrEmpty { page.url },
imageUrl = page.preview.ifNullOrEmpty { pageLoader.getPageUrl(page) },
createdAt = Date(),
percent = computePercent(state.chapterId, state.page),
)
@@ -312,10 +312,8 @@ 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)

View File

@@ -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 loadPreview(page: MangaPage) {
val data: Any = page.preview?.takeUnless { it.isEmpty() } ?: page
private fun onPreviewChanged(preview: MangaPage?) {
if (preview == null) return
ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(data)
.data(preview)
.scale(Scale.FILL)
.decodeRegion()
.tag(page.source)
.tag(preview.source)
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(viewBinding.imageViewBefore))

View File

@@ -7,10 +7,11 @@ 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.core.util.ext.require
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
import javax.inject.Inject
@@ -18,24 +19,38 @@ import javax.inject.Inject
@HiltViewModel
class ColorFilterConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() {
private val manga = savedStateHandle.require<ParcelableManga>(EXTRA_MANGA).manga
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
private var initialColorFilter: ReaderColorFilter? = null
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
val onDismiss = MutableEventFlow<Unit>()
val preview = savedStateHandle.require<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES).pages.first()
val preview = MutableStateFlow<MangaPage?>(null)
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) {

View File

@@ -17,13 +17,9 @@ 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)
@@ -38,7 +34,6 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
override fun onDestroyView() {
stateToSave = getCurrentState()
readerAdapter = null
super.onDestroyView()
}
@@ -50,10 +45,6 @@ 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)
@@ -64,7 +55,5 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
abstract fun getCurrentState(): ReaderState?
protected abstract fun onCreateAdapter(): BaseReaderAdapter<*>
protected abstract suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
}

View File

@@ -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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.File
import java.io.IOException

View File

@@ -4,18 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -35,6 +33,8 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
@Inject
lateinit var pageLoader: PageLoader
private var pagerAdapter: ReversedPagesAdapter? = null
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -42,8 +42,15 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.pager) {
adapter = readerAdapter
adapter = pagerAdapter
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
}
@@ -60,18 +67,11 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
}
override fun onDestroyView() {
pagerAdapter = null
requireViewBinding().pager.adapter = null
super.onDestroyView()
}
override fun onCreateAdapter() = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
override fun switchPageBy(delta: Int) {
with(requireViewBinding().pager) {
setCurrentItem(currentItem - delta, context.isAnimationsEnabled)
@@ -87,26 +87,24 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
}
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
val reversedPages = pages.asReversed()
val items = async {
requireAdapter().setItems(reversedPages)
yield()
}
if (pendingState != null) {
val position = reversedPages.indexOfLast {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
viewLifecycleScope.launch {
val items = async {
pagerAdapter?.setItems(reversedPages)
}
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
if (pendingState != null) {
val position = reversedPages.indexOfLast {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
items.await() ?: return@launch
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
}
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
items.await()
}
} else {
items.await()
}
}
@@ -125,6 +123,6 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
}
private fun reversed(position: Int): Int {
return ((readerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
}
}

View File

@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.util.ext.*
open class PageHolder(
owner: LifecycleOwner,

View File

@@ -4,18 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -34,6 +32,8 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
@Inject
lateinit var pageLoader: PageLoader
private var pagesAdapter: PagesAdapter? = null
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -41,8 +41,15 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
pagesAdapter = PagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.pager) {
adapter = readerAdapter
adapter = pagesAdapter
offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged)
}
@@ -59,40 +66,31 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
}
override fun onDestroyView() {
pagesAdapter = null
requireViewBinding().pager.adapter = null
super.onDestroyView()
}
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val items = async {
requireAdapter().setItems(pages)
yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
viewLifecycleScope.launch {
val items = async {
pagesAdapter?.setItems(pages)
}
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
items.await() ?: return@launch
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
}
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
items.await()
}
} else {
items.await()
}
}
override fun onCreateAdapter() = PagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
override fun switchPageBy(delta: Int) {
with(requireViewBinding().pager) {
setCurrentItem(currentItem + delta, context.isAnimationsEnabled)

View File

@@ -4,17 +4,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -33,6 +31,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
lateinit var pageLoader: PageLoader
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
override fun onCreateViewBinding(
inflater: LayoutInflater,
@@ -41,9 +40,16 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
webtoonAdapter = WebtoonAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = readerAdapter
adapter = webtoonAdapter
addOnPageScrollListener(PageScrollListener())
}
@@ -53,43 +59,32 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
}
override fun onDestroyView() {
webtoonAdapter = null
requireViewBinding().recyclerView.adapter = null
super.onDestroyView()
}
override fun onCreateAdapter() = WebtoonAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val setItems = async {
requireAdapter().setItems(pages)
yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
setItems.await()
if (position != -1) {
with(requireViewBinding().recyclerView) {
firstVisibleItemPosition = position
post {
(findViewHolderForAdapterPosition(position) as? WebtoonHolder)
?.restoreScroll(pendingState.scroll)
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
viewLifecycleScope.launch {
val setItems = async { webtoonAdapter?.setItems(pages) }
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
setItems.await() ?: return@launch
if (position != -1) {
with(requireViewBinding().recyclerView) {
firstVisibleItemPosition = position
post {
(findViewHolderForAdapterPosition(position) as? WebtoonHolder)
?.restoreScroll(pendingState.scroll)
}
}
notifyPageChanged(position)
}
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
setItems.await()
}
} else {
setItems.await()
}
}

View File

@@ -30,7 +30,7 @@ import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import javax.inject.Inject
@@ -101,8 +101,8 @@ class PagesThumbnailsSheet :
listener.onPageSelected(item.page)
} else {
val state = ReaderState(item.page.chapterId, item.page.index, 0)
val intent = IntentBuilder(view.context).manga(viewModel.manga).state(state).build()
startActivity(intent, scaleUpActivityOptionsOf(view))
val intent = ReaderActivity.newIntent(view.context, viewModel.manga, state)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
}
dismiss()
}

View File

@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -27,7 +26,7 @@ class PagesThumbnailsViewModel @Inject constructor(
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
val manga = requireNotNull(savedStateHandle.get<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA)).manga
private val repository = mangaRepositoryFactory.create(manga.source)
private val mangaDetails = SuspendLazy {

View File

@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
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.call
import org.koitharu.kotatsu.core.util.ext.require
@@ -36,7 +37,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi
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.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L
@@ -46,6 +47,7 @@ open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val filter: FilterCoordinator,
private val tagHighlighter: MangaTagHighlighter,
settings: AppSettings,
listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler,
@@ -70,7 +72,7 @@ open class RemoteListViewModel @Inject constructor(
list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags))
else -> {
list.toUi(this, mode, listExtraProvider)
list.toUi(this, mode, listExtraProvider, tagHighlighter)
when {
error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter())

View File

@@ -16,6 +16,10 @@ import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuAuthenticator
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuInterceptor
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
import org.koitharu.kotatsu.scrobbling.kitsu.domain.KitsuScrobbler
import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator
import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor
import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler
@@ -64,6 +68,24 @@ object ScrobblingModule {
addInterceptor(AniListInterceptor(storage))
}.build()
@Provides
@Singleton
fun provideKitsuRepository(
@ApplicationContext context: Context,
@ScrobblerType(ScrobblerService.KITSU) storage: ScrobblerStorage,
database: MangaDatabase,
authenticator: KitsuAuthenticator,
): KitsuRepository {
val okHttp = OkHttpClient.Builder().apply {
authenticator(authenticator)
addInterceptor(KitsuInterceptor(storage))
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}
}.build()
return KitsuRepository(context, okHttp, storage, database)
}
@Provides
@Singleton
@ScrobblerType(ScrobblerService.ANILIST)
@@ -85,11 +107,19 @@ object ScrobblingModule {
@ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.MAL)
@Provides
@Singleton
@ScrobblerType(ScrobblerService.KITSU)
fun provideKitsuStorage(
@ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.KITSU)
@Provides
@ElementsIntoSet
fun provideScrobblers(
shikimoriScrobbler: ShikimoriScrobbler,
aniListScrobbler: AniListScrobbler,
malScrobbler: MALScrobbler,
): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler)
kitsuScrobbler: KitsuScrobbler
): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, aniListScrobbler, malScrobbler, kitsuScrobbler)
}

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.scrobbling.anilist.data
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
enum class ScoreFormat {

View File

@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.util.ext.findKeyByValue
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
@@ -21,7 +20,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.EnumMap
abstract class Scrobbler(
@@ -124,7 +123,7 @@ abstract class Scrobbler(
rating = rating,
title = mangaInfo.name,
coverUrl = mangaInfo.cover,
description = mangaInfo.descriptionHtml.parseAsHtml().sanitize(),
description = mangaInfo.descriptionHtml.parseAsHtml(),
externalUrl = mangaInfo.url,
)
}

View File

@@ -12,5 +12,6 @@ enum class ScrobblerService(
SHIKIMORI(1, R.string.shikimori, R.drawable.ic_shikimori),
ANILIST(2, R.string.anilist, R.drawable.ic_anilist),
MAL(3, R.string.mal, R.drawable.ic_mal)
MAL(3, R.string.mal, R.drawable.ic_mal),
KITSU(4, R.string.kitsu, R.drawable.ic_kitsu)
}

View File

@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject
@HiltViewModel

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.scrobbling.kitsu.data
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
import javax.inject.Inject
import javax.inject.Provider
class KitsuAuthenticator @Inject constructor(
@ScrobblerType(ScrobblerService.KITSU) private val storage: ScrobblerStorage,
private val repositoryProvider: Provider<KitsuRepository>,
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.scrobbling.kitsu.data
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
private const val JSON = "application/json"
class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val sourceRequest = chain.request()
val request = sourceRequest.newBuilder()
request.header(CommonHeaders.CONTENT_TYPE, JSON)
request.header(CommonHeaders.ACCEPT, JSON)
if (!sourceRequest.url.pathSegments.contains("oauth")) {
storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
}
}
return chain.proceed(request.build())
}
}

View File

@@ -0,0 +1,92 @@
package org.koitharu.kotatsu.scrobbling.kitsu.data
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
private const val BASE_WEB_URL = "https://kitsu.io"
class KitsuRepository(
@ApplicationContext context: Context,
private val okHttp: OkHttpClient,
private val storage: ScrobblerStorage,
private val db: MangaDatabase,
) : ScrobblerRepository {
private val clientId = context.getString(R.string.kitsu_clientId)
private val clientSecret = context.getString(R.string.kitsu_clientSecret)
override val oauthUrl: String = "kotatsu+kitsu://auth"
override val isAuthorized: Boolean
get() = storage.accessToken != null
override val cachedUser: ScrobblerUser?
get() {
return storage.user
}
override suspend fun authorize(code: String?) {
val body = FormBody.Builder()
if (code != null) {
body.add("grant_type", "password")
body.add("username", "test@test")
body.add("password", "test")
} else {
body.add("grant_type", "refresh_token")
body.add("refresh_token", checkNotNull(storage.refreshToken))
}
val request = Request.Builder()
.post(body.build())
.url("${BASE_WEB_URL}/api/oauth/token")
val response = okHttp.newCall(request.build()).await().parseJson()
storage.accessToken = response.getString("access_token")
storage.refreshToken = response.getString("refresh_token")
}
override suspend fun loadUser(): ScrobblerUser {
TODO("Not yet implemented")
}
override fun logout() {
TODO("Not yet implemented")
}
override suspend fun unregister(mangaId: Long) {
return db.scrobblingDao.delete(ScrobblerService.KITSU.id, mangaId)
}
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
TODO("Not yet implemented")
}
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
TODO("Not yet implemented")
}
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
TODO("Not yet implemented")
}
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
TODO("Not yet implemented")
}
override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.scrobbling.kitsu.domain
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository
import javax.inject.Inject
class KitsuScrobbler @Inject constructor(
private val repository: KitsuRepository,
db: MangaDatabase,
) : Scrobbler(db, ScrobblerService.KITSU, repository) {
init {
statuses[ScrobblingStatus.PLANNED] = "planned"
statuses[ScrobblingStatus.READING] = "current"
statuses[ScrobblingStatus.COMPLETED] = "completed"
statuses[ScrobblingStatus.ON_HOLD] = "on_hold"
statuses[ScrobblingStatus.DROPPED] = "dropped"
}
override suspend fun updateScrobblingInfo(
mangaId: Long,
rating: Float,
status: ScrobblingStatus?,
comment: String?
) {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId)
requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" }
repository.updateRate(
rateId = entity.id,
mangaId = entity.mangaId,
rating = rating,
status = statuses[status],
comment = comment,
)
}
}

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.scrobbling.kitsu.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.graphics.Insets
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityKitsuAuthBinding
class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityKitsuAuthBinding.inflate(layoutInflater))
}
override fun onWindowInsetsChanged(insets: Insets) {
val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
binding.root.setPadding(
basePadding + insets.left,
basePadding + insets.top,
basePadding + insets.right,
basePadding + insets.bottom,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, KitsuAuthActivity::class.java)
}
}

View File

@@ -13,10 +13,10 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
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.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
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
@@ -33,7 +33,7 @@ class SearchViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
repositoryFactory: MangaRepository.Factory,
settings: AppSettings,
private val extraProvider: ListExtraProvider,
private val tagHighlighter: MangaTagHighlighter,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
@@ -64,7 +64,7 @@ class SearchViewModel @Inject constructor(
else -> {
val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode, extraProvider)
list.toUi(result, mode, tagHighlighter)
when {
error != null -> result += error.toErrorFooter()
hasNext -> result += LoadingFooter()

Some files were not shown because too many files have changed in this diff Show More