Compare commits

..

60 Commits

Author SHA1 Message Date
627a00beb4 Update parsers 2022-10-01 14:00:11 +03:00
e00ed13ad1 Fix badge alignment on details screen 2022-10-01 13:06:00 +03:00
893fa6bd90 Fix opening fingerprint dialog 2022-10-01 12:19:24 +03:00
512188c8dd Option to disable reader slider #228 2022-10-01 11:42:28 +03:00
aae6761809 Remove unused code 2022-10-01 10:39:58 +03:00
c3f055d0c4 Fix widgets colors 2022-10-01 10:39:58 +03:00
Tony
04d5df20d1 Translated using Weblate (Portuguese)
Currently translated at 69.0% (268 of 388 strings)

Translated using Weblate (Portuguese)

Currently translated at 68.8% (267 of 388 strings)

Co-authored-by: Tony <t.tony.br01@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-10-01 10:23:12 +03:00
Eric
665eca0699 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (8 of 8 strings)

Co-authored-by: Eric <hamburger1024@mailbox.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hans/
Translation: Kotatsu/plurals
2022-10-01 10:23:12 +03:00
Faris Daffa
9a1534464f Translated using Weblate (Indonesian)
Currently translated at 93.2% (362 of 388 strings)

Co-authored-by: Faris Daffa <faris.6dsdiaf@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2022-10-01 10:23:12 +03:00
kuragehime
f856fc6fac Translated using Weblate (Chinese (Traditional))
Currently translated at 27.5% (107 of 388 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 26.2% (102 of 388 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (388 of 388 strings)

Added translation using Weblate (Chinese (Traditional))

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2022-10-01 10:23:12 +03:00
4af8e73303 Fix crashes in CoroutineIntentService 2022-10-01 09:28:11 +03:00
23239f1fec Add setReorderingAllowed for fragment transactions 2022-09-23 18:30:38 +03:00
853e4d6fde Follow guidelines for some dialogs 2022-09-22 20:00:43 +03:00
14a37ad16e Minor adjustments 2022-09-22 20:00:34 +03:00
d604ff3c24 Update ssiv 2022-09-22 16:28:25 +03:00
Sergio Varela
4f9eee7d46 Translated using Weblate (Spanish)
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-09-22 16:24:06 +03:00
Zero O
fe673a94ed Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: Zero O <godarms2010@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-09-22 16:24:06 +03:00
bcd891d653 Upgrade plugins 2022-09-21 20:48:29 +03:00
1e75edf262 Fix coroutines cancellation 2022-09-21 18:50:20 +03:00
73478d6a81 Fix getParcelableCompat extensions 2022-09-19 10:06:43 +03:00
982080a930 Update ssiv 2022-09-18 13:26:25 +03:00
66cd5070dc Update FUNDING.yml 2022-09-17 10:53:46 +03:00
vianh
c05b0eaa59 Move fallback inset to dimens 2022-09-16 08:29:54 +03:00
vianh
29a073d844 Adjust reader info bar 2022-09-16 08:29:54 +03:00
a0e69428e4 Widget theme fix #225 2022-09-16 08:29:32 +03:00
3f6a103915 Rename Library to Shelf 2022-09-14 18:46:32 +03:00
734765dbdd Fix suggestions worker processing broken tags 2022-09-14 14:09:02 +03:00
Marcos Souza
117c4c5978 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 87.3% (339 of 388 strings)

Co-authored-by: Marcos Souza <marcossouzacg2004@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
kyoya
13e33a6614 Translated using Weblate (Turkish)
Currently translated at 99.2% (385 of 388 strings)

Co-authored-by: kyoya <thelol9181@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
Dpper
affb495136 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
demo_101
9b2dafd668 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.6% (379 of 388 strings)

Co-authored-by: demo_101 <sleepy@nixnet.email>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2022-09-14 13:50:13 +03:00
5f32c0401f Update ssiv 2022-09-14 13:51:11 +03:00
f35f40ed27 Merge branch 'master' into devel 2022-09-14 12:58:11 +03:00
46f0d3ef74 Fix onboarding dialog 2022-09-14 12:34:33 +03:00
c27c785ac2 Use Coil for empty states images 2022-09-14 12:30:07 +03:00
6e844e8c3b Fix library selection #224 2022-09-12 16:24:14 +03:00
4186c36f30 Prevent GoneOnInvisibleListener leak 2022-09-12 15:45:34 +03:00
2d727a0da8 Prevent GoneOnInvisibleListener leak 2022-09-12 15:43:42 +03:00
39e574e9dc Update dependencies 2022-09-12 15:40:37 +03:00
efb94cbd67 Fix unwanted errors reporting 2022-09-12 14:34:07 +03:00
757e33dfb4 Fix unwanted errors reporting 2022-09-12 14:32:42 +03:00
ab9bdf9f07 Fixes 2022-09-11 09:11:35 +03:00
a68632a888 Update SubsamplingScaleImageView 2022-09-10 16:47:50 +03:00
9b0dc8b413 Fix hiding FastScroller 2022-09-08 15:53:16 +03:00
f43fe5830e Fix crashes 2022-09-08 15:39:12 +03:00
ce616b328c Update dependencies 2022-09-08 14:46:27 +03:00
824a8ff97a Merge branch 'master' into devel 2022-09-08 14:42:01 +03:00
no
ef9018b92f Translated using Weblate (Turkish)
Currently translated at 90.2% (350 of 388 strings)

Co-authored-by: no <cbasroyalasroyal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-08 14:38:29 +03:00
J. Lavoie
a088d10935 Translated using Weblate (French)
Currently translated at 100.0% (388 of 388 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2022-09-08 14:38:29 +03:00
2e561697ac Update parsers 2022-09-07 14:07:48 +03:00
d242acd502 Sort local list by manga name 2022-09-03 15:57:48 +03:00
d37b44d3f6 Update parsers 2022-09-03 15:56:16 +03:00
6243fc88c9 Sorting local manga #219 #217 2022-09-03 15:45:06 +03:00
Neko Nekowazarashi
f74e865b06 Translated using Weblate (Indonesian)
Currently translated at 91.7% (356 of 388 strings)

Co-authored-by: Neko Nekowazarashi <kodra@nekoweb.my.id>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
no
1a2bc02188 Translated using Weblate (Turkish)
Currently translated at 88.9% (345 of 388 strings)

Co-authored-by: no <cbasroyalasroyal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
b0153e9f61 Translated using Weblate (Russian)
Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (388 of 388 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
J. Lavoie
7f3fc1b88a Translated using Weblate (French)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
Zero O
40bf0c75bf Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Added translation using Weblate (Chinese (Simplified))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Zero O <godarms2010@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-09-03 13:45:59 +03:00
Dpper
9407bd205c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
kuragehime
3317c8dddc Translated using Weblate (Japanese)
Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-09-03 13:45:59 +03:00
145 changed files with 1614 additions and 685 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
custom: ["https://yoomoney.ru/to/410012543938752"]
ko_fi: koitharu

3
.idea/kotlinc.xml generated
View File

@@ -3,4 +3,7 @@
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.7.10" />
</component>
</project>

6
.idea/ktlint.xml generated
View File

@@ -1,7 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtlintProjectConfiguration">
<enableKtlint>false</enableKtlint>
<androidMode>true</androidMode>
<treatAsErrors>false</treatAsErrors>
<disabledRules>
<list>
<option value="no-empty-first-line-in-method-block" />
</list>
</disabledRules>
</component>
</project>

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 494
versionName '4.0-a5'
versionCode 499
versionName '4.0-rc1'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -60,6 +60,7 @@ android {
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil.annotation.ExperimentalCoilApi',
'-opt-in=com.google.android.material.badge.ExperimentalBadgeUtils',
]
}
lint {
@@ -82,15 +83,15 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
implementation('com.github.KotatsuApp:kotatsu-parsers:b1990c7918') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.activity:activity-ktx:1.5.1'
implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-ktx:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
@@ -101,8 +102,8 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
implementation 'com.google.android.material:material:1.7.0-beta01'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.7.0-rc01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
@@ -117,15 +118,14 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation "com.google.dagger:hilt-android:2.43.2"
kapt "com.google.dagger:hilt-compiler:2.43.2"
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.2.0'
implementation 'io.coil-kt:coil-svg:2.2.0'
// implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2942b797a2'
implementation 'io.coil-kt:coil-base:2.2.1'
implementation 'io.coil-kt:coil-svg:2.2.1'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0ff0278f0f'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'ch.acra:acra-http:5.9.6'
@@ -145,8 +145,8 @@ dependencies {
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
}

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.base.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
fun interface ReversibleHandle {
@@ -11,8 +14,10 @@ fun interface ReversibleHandle {
}
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
runCatching {
reverse()
runCatchingCancellable {
withContext(NonCancellable) {
reverse()
}
}.onFailure {
it.printStackTraceDebug()
}
@@ -21,4 +26,4 @@ fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.D
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
this.reverse()
other.reverse()
}
}

View File

@@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
@@ -21,7 +20,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import dagger.hilt.android.EntryPointAccessors
import javax.inject.Inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
@@ -31,6 +29,7 @@ import org.koitharu.kotatsu.base.ui.util.inject
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.getThemeColor
import javax.inject.Inject
abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(),
@@ -92,7 +91,8 @@ abstract class BaseActivity<B : ViewBinding> :
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
ActivityCompat.recreate(this)
// ActivityCompat.recreate(this)
TODO("Test error")
return true
}
return super.onKeyDown(keyCode, event)
@@ -134,6 +134,7 @@ abstract class BaseActivity<B : ViewBinding> :
window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
}
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&

View File

@@ -4,11 +4,13 @@ import android.app.Service
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class CoroutineIntentService : BaseService() {
@@ -21,11 +23,13 @@ abstract class CoroutineIntentService : BaseService() {
return Service.START_REDELIVER_INTENT
}
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch(errorHandler(startId)) {
mutex.withLock {
try {
withContext(dispatcher) {
processIntent(intent)
if (intent != null) {
withContext(dispatcher) {
processIntent(startId, intent)
}
}
} finally {
stopSelf(startId)
@@ -33,5 +37,12 @@ abstract class CoroutineIntentService : BaseService() {
}
}
protected abstract suspend fun processIntent(intent: Intent?)
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
protected abstract fun onError(startId: Int, error: Throwable)
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug()
onError(startId, throwable)
}
}

View File

@@ -60,7 +60,19 @@ class SectionedSelectionController<T : Any>(
startActionMode()
notifySelectionChanged()
}
recyclerView.addItemDecoration(decoration)
var shouldAddDecoration = true
for (i in (0 until recyclerView.itemDecorationCount).reversed()) {
val decor = recyclerView.getItemDecorationAt(i)
if (decor === decoration) {
shouldAddDecoration = false
break
} else if (decor.javaClass == decoration.javaClass) {
recyclerView.removeItemDecorationAt(i)
}
}
if (shouldAddDecoration) {
recyclerView.addItemDecoration(decoration)
}
if (pendingData?.isEmpty() == true) {
pendingData = null
}

View File

@@ -33,6 +33,7 @@ class ScrollbarAnimator(
.animate()
.translationX(0f)
.alpha(1f)
.setListener(null)
.setDuration(animationDuration)
}

View File

@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@@ -12,7 +13,6 @@ import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics
class BookmarksGroupAdapter(
coil: ImageLoader,
@@ -34,11 +34,11 @@ class BookmarksGroupAdapter(
selectionController = selectionController,
bookmarkClickListener = bookmarkClickListener,
groupClickListener = groupClickListener,
)
),
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(listener))
.addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener))
}
@@ -64,4 +64,4 @@ class BookmarksGroupAdapter(
}
}
}
}
}

View File

@@ -1,13 +1,14 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import javax.inject.Inject
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
private const val PAGE_SIZE = 10
@@ -85,7 +86,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity()
}
val history = JsonDeserializer(item).toHistoryEntity()
result += runCatching {
result += runCatchingCancellable {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
@@ -100,7 +101,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatching {
result += runCatchingCancellable {
db.favouriteCategoriesDao.upsert(category)
}
}
@@ -116,7 +117,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity()
}
val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatching {
result += runCatchingCancellable {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)

View File

@@ -6,6 +6,9 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
@@ -17,9 +20,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.isSuccess
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver private constructor(
private val activity: FragmentActivity?,
@@ -65,7 +65,7 @@ class ExceptionResolver private constructor(
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismiss()
dialog.dismissAllowingStateLoss()
}
}
}
@@ -94,4 +94,4 @@ class ExceptionResolver private constructor(
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
}
}
}

View File

@@ -4,13 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -24,6 +17,14 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
@@ -59,7 +60,7 @@ class AppUpdateRepository @Inject constructor(
if (!isUpdateSupported()) {
return@withContext null
}
runCatching {
runCatchingCancellable {
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
val available = getAvailableVersions().asArrayList()
available.sortBy { it.versionId }

View File

@@ -2,7 +2,12 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.readParcelableCompat
import org.koitharu.kotatsu.utils.ext.readSerializableCompat
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
out.writeLong(id)
@@ -37,11 +42,11 @@ fun Parcel.readManga() = Manga(
coverUrl = requireNotNull(readString()),
largeCoverUrl = readString(),
description = readString(),
tags = requireNotNull(readParcelable<ParcelableMangaTags>(ParcelableMangaTags::class.java.classLoader)).tags,
state = readSerializable() as MangaState?,
tags = requireNotNull(readParcelableCompat<ParcelableMangaTags>()).tags,
state = readSerializableCompat(),
author = readString(),
chapters = readParcelable<ParcelableMangaChapters>(ParcelableMangaChapters::class.java.classLoader)?.chapters,
source = readSerializable() as MangaSource,
chapters = readParcelableCompat<ParcelableMangaChapters>()?.chapters,
source = checkNotNull(readSerializableCompat()),
)
fun MangaPage.writeToParcel(out: Parcel) {
@@ -57,7 +62,7 @@ fun Parcel.readMangaPage() = MangaPage(
url = requireNotNull(readString()),
referer = requireNotNull(readString()),
preview = readString(),
source = readSerializable() as MangaSource,
source = checkNotNull(readSerializableCompat()),
)
fun MangaChapter.writeToParcel(out: Parcel) {
@@ -79,7 +84,7 @@ fun Parcel.readMangaChapter() = MangaChapter(
scanlator = readString(),
uploadDate = readLong(),
branch = readString(),
source = readSerializable() as MangaSource,
source = checkNotNull(readSerializableCompat()),
)
fun MangaTag.writeToParcel(out: Parcel) {
@@ -91,5 +96,5 @@ fun MangaTag.writeToParcel(out: Parcel) {
fun Parcel.readMangaTag() = MangaTag(
title = requireNotNull(readString()),
key = requireNotNull(readString()),
source = readSerializable() as MangaSource,
)
source = checkNotNull(readSerializableCompat()),
)

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe
private const val MAX_SAFE_CHAPTERS_COUNT = 32 // this is 100% safe
class ParcelableManga(
val manga: Manga,
@@ -50,4 +50,4 @@ class ParcelableManga(
return arrayOfNulls(size)
}
}
}
}

View File

@@ -4,7 +4,6 @@ import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils
import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
@@ -15,9 +14,9 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -31,6 +30,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ShortcutsUpdater @Inject constructor(
@@ -91,7 +93,7 @@ class ShortcutsUpdater @Inject constructor(
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatching {
private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
.filter { x -> x.title.isNotEmpty() }
@@ -111,14 +113,15 @@ class ShortcutsUpdater @Inject constructor(
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching {
val bmp = coil.execute(
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.precision(Precision.EXACT)
.scale(Scale.FILL)
.build(),
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },

View File

@@ -11,20 +11,23 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue
import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Collections
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
@@ -205,9 +208,16 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReaderBarEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_BAR, true)
val isReaderSliderEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
val dnsOverHttps: DoHProvider
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
var localListOrder: SortOrder
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
NETWORK_ALWAYS -> true
@@ -323,8 +333,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_INCOGNITO_MODE = "incognito"
const val KEY_SYNC = "sync"
const val KEY_READER_BAR = "reader_bar"
const val KEY_READER_SLIDER = "reader_slider"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -54,7 +54,7 @@ class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.report) { _, _ ->
dismiss()
error.report(TAG)
error.report()
}.setTitle(R.string.error_occurred)
}

View File

@@ -6,8 +6,6 @@ import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
@@ -202,18 +200,4 @@ class ChaptersFragment :
private fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
}
private fun findBottomSheetBehavior(): BottomSheetBehavior<*>? {
val v = view ?: return null
for (p in v.parents) {
val layoutParams = (p as? View)?.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is BottomSheetBehavior<*>) {
return behavior
}
}
}
return null
}
}

View File

@@ -23,8 +23,8 @@ class ChaptersMenuProvider(
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {

View File

@@ -19,12 +19,10 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
@@ -37,12 +35,17 @@ import org.koitharu.kotatsu.core.ui.MangaErrorDialog
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.adapter.bindBadge
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ViewBadge
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.utils.ext.textAndVisible
import javax.inject.Inject
@AndroidEntryPoint
class DetailsActivity :
@@ -60,7 +63,7 @@ class DetailsActivity :
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
private var badge: BadgeDrawable? = null
private lateinit var viewBadge: ViewBadge
private val viewModel: DetailsViewModel by assistedViewModels {
viewModelFactory.create(MangaIntent(intent))
@@ -83,6 +86,7 @@ class DetailsActivity :
}
binding.buttonRead.setOnClickListener(this)
binding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(binding.buttonRead, this)
chaptersMenuProvider = if (binding.layoutBottom != null) {
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
@@ -151,6 +155,7 @@ class DetailsActivity :
)
}
}
R.id.button_dropdown -> showBranchPopupMenu()
}
}
@@ -188,10 +193,12 @@ class DetailsActivity :
ExceptionResolver.canResolve(e) -> {
resolveError(e)
}
manga == null -> {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
else -> {
val snackbar = makeSnackbar(
e.getDisplayMessage(resources),
@@ -242,7 +249,7 @@ class DetailsActivity :
}
private fun onNewChaptersChanged(newChapters: Int) {
badge = binding.buttonRead.bindBadge(badge, newChapters)
viewBadge.counter = newChapters
}
fun showChapterMissingDialog(chapterId: Long) {

View File

@@ -45,6 +45,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class DetailsViewModel @AssistedInject constructor(
@Assisted intent: MangaIntent,
@@ -189,7 +190,7 @@ class DetailsViewModel @AssistedInject constructor(
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
runCatching {
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
@@ -228,7 +229,7 @@ class DetailsViewModel @AssistedInject constructor(
reload()
} else {
viewModelScope.launch(Dispatchers.Default) {
runCatching {
runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
delegate.relatedManga.value = it

View File

@@ -17,6 +17,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.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaDetailsDelegate(
private val intent: MangaIntent,
@@ -45,9 +46,9 @@ class MangaDetailsDelegate(
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga
relatedManga.value = runCatching {
relatedManga.value = runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
mangaRepositoryFactory.create(m.source).getDetails(m)
} else {
localMangaRepository.findSavedManga(manga)

View File

@@ -9,11 +9,18 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import kotlinx.coroutines.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.closeQuietly
@@ -32,7 +39,9 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import java.io.File
private const val MAX_FAILSAFE_ATTEMPTS = 2
private const val DOWNLOAD_ERROR_DELAY = 500L
@@ -155,7 +164,7 @@ class DownloadManager @AssistedInject constructor(
}
outState.value = DownloadState.PostProcessing(startId, data, cover)
output.mergeWithExisting()
output.finalize()
output.finish()
val localManga = localMangaRepository.getFromFile(output.file)
outState.value = DownloadState.Done(startId, data, cover, localManga)
} catch (e: CancellationException) {
@@ -231,7 +240,7 @@ class DownloadManager @AssistedInject constructor(
)
}
private suspend fun loadCover(manga: Manga) = runCatching {
private suspend fun loadCover(manga: Manga) = runCatchingCancellable {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)

View File

@@ -34,7 +34,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = DownloadsAdapter(lifecycleScope, coil)
val spacing = resources.getDimensionPixelOffset(R.dimen.grid_spacing)
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter

View File

@@ -16,10 +16,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.set
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
@@ -28,10 +30,14 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.throttle
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import org.koitharu.kotatsu.utils.progress.ProgressJob
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.set
@AndroidEntryPoint
class DownloadService : BaseService() {
@@ -65,7 +71,7 @@ class DownloadService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga
val manga = intent?.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)
return if (manga != null) {
jobs[startId] = downloadManga(startId, manga, chapters)
@@ -84,7 +90,9 @@ class DownloadService : BaseService() {
override fun onDestroy() {
unregisterReceiver(controlReceiver)
wakeLock.release()
if (wakeLock.isHeld) {
wakeLock.release()
}
isRunning = false
super.onDestroy()
}
@@ -168,6 +176,7 @@ class DownloadService : BaseService() {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.cancel()
}
ACTION_DOWNLOAD_RESUME -> {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.resume()
@@ -254,7 +263,7 @@ class DownloadService : BaseService() {
fun getDownloadedManga(intent: Intent?): Manga? {
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
return intent.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga
return intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
}
return null
}

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
@@ -28,6 +29,7 @@ class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
replace(R.id.container, fragment)
}

View File

@@ -4,12 +4,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter(
coil: ImageLoader,
@@ -20,7 +20,7 @@ class CategoriesAdapter(
init {
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener))
.addDelegate(emptyStateListAD(listListener))
.addDelegate(emptyStateListAD(coil, listListener))
.addDelegate(loadingStateAD())
}
@@ -56,4 +56,4 @@ class CategoriesAdapter(
}
}
}
}
}

View File

@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class FavouritesListViewModel @AssistedInject constructor(
@Assisted private val categoryId: Long,
@@ -69,6 +70,7 @@ class FavouritesListViewModel @AssistedInject constructor(
actionStringRes = 0,
),
)
else -> list.toUi(mode, this)
}
}.catch {
@@ -79,7 +81,7 @@ class FavouritesListViewModel @AssistedInject constructor(
if (categoryId != NO_ID) {
launchJob {
categoryName = withContext(Dispatchers.Default) {
runCatching {
runCatchingCancellable {
repository.getCategory(categoryId).title
}.getOrNull()
}

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class HistoryActivity :
@@ -28,6 +29,7 @@ class HistoryActivity :
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = HistoryListFragment.newInstance()
replace(R.id.container, fragment)
}

View File

@@ -5,9 +5,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import com.google.android.material.R as materialR
class HistoryListMenuProvider(
private val context: Context,
@@ -38,6 +38,6 @@ class HistoryListMenuProvider(
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
}
}

View File

@@ -79,7 +79,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
private fun setDrawable(drawable: Drawable?) {
if (drawable != null) {
view.setImage(ImageSource.bitmap(drawable.toBitmap()))
view.setImage(ImageSource.Bitmap(drawable.toBitmap()))
} else {
view.recycle()
}

View File

@@ -1,15 +0,0 @@
package org.koitharu.kotatsu.library.ui.adapter
import android.view.View
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga
interface LibraryListEventListener : ListStateHolderListener {
fun onItemClick(item: Manga, section: LibrarySectionModel, view: View)
fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean
fun onSectionClick(section: LibrarySectionModel, view: View)
}

View File

@@ -1,13 +1,13 @@
@file:SuppressLint("UnsafeOptInUsageError")
package org.koitharu.kotatsu.list.ui.adapter
import android.annotation.SuppressLint
import android.view.View
import androidx.annotation.CheckResult
import androidx.core.view.doOnNextLayout
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import org.koitharu.kotatsu.R
@CheckResult
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
return if (counter > 0) {
val badgeDrawable = badge ?: initBadge(this)
@@ -39,4 +39,4 @@ private fun initBadge(anchor: View): BadgeDrawable {
private fun BadgeDrawable.align() {
horizontalOffset = intrinsicWidth
verticalOffset = intrinsicHeight
}
}

View File

@@ -1,23 +1,31 @@
package org.koitharu.kotatsu.list.ui.adapter
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyStateListAD(
coil: ImageLoader,
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) }
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.icon.newImageRequest(item.icon)?.enqueueWith(coil)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
}
onViewRecycled {
binding.icon.disposeImageRequest()
}
}

View File

@@ -4,9 +4,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.*
import kotlin.jvm.internal.Intrinsics
open class MangaListAdapter(
coil: ImageLoader,
@@ -24,7 +24,7 @@ open class MangaListAdapter(
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
}

View File

@@ -46,7 +46,7 @@ fun mangaListDetailedItemAD(
}
binding.textViewRating.textAndVisible = item.rating
binding.textViewTags.text = item.tags
itemView.bindBadge(badge, item.counter)
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {

View File

@@ -40,7 +40,7 @@ fun mangaListItemAD(
lifecycle(lifecycleOwner)
enqueueWith(coil)
}
itemView.bindBadge(badge, item.counter)
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {

View File

@@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.text.Collator
import java.util.*
import java.util.Locale
import java.util.TreeSet
class FilterCoordinator(
private val repository: RemoteMangaRepository,
@@ -153,7 +160,7 @@ class FilterCoordinator(
}
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
runCatching {
runCatchingCancellable {
repository.getTags()
}.onFailure { error ->
error.printStackTraceDebug()
@@ -204,4 +211,4 @@ class FilterCoordinator(
return collator?.compare(t1, t2) ?: compareValues(t1, t2)
}
}
}
}

View File

@@ -62,7 +62,7 @@ class CbzMangaOutput(
index.addChapter(chapter)
}
suspend fun finalize() {
suspend fun finish() {
runInterruptible(Dispatchers.IO) {
output.put(ENTRY_NAME_INDEX, index.toString())
output.finish()

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.local.domain
import java.io.File
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
class LocalManga(
val manga: Manga,
val file: File,
) {
var createdAt: Long = -1L
get() {
if (field == -1L) {
field = file.lastModified()
}
return field
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LocalManga
if (manga != other.manga) return false
if (file != other.file) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + file.hashCode()
return result
}
}
fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }
fun LocalManga.isMatchesQuery(query: String): Boolean {
return manga.title.contains(query, ignoreCase = true) ||
manga.altTitle?.contains(query, ignoreCase = true) == true
}
fun LocalManga.containsTags(tags: Set<MangaTag>): Boolean {
return manga.tags.containsAll(tags)
}

View File

@@ -7,28 +7,40 @@ import androidx.annotation.WorkerThread
import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import java.io.File
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
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.toCamelCase
import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.CompositeMutex
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.File
import java.util.Enumeration
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
private const val MAX_PARALLELISM = 4
@@ -45,12 +57,9 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
val list = getRawList()
if (query.isNotEmpty()) {
list.retainAll { x ->
x.title.contains(query, ignoreCase = true) ||
x.altTitle?.contains(query, ignoreCase = true) == true
}
list.retainAll { x -> x.isMatchesQuery(query) }
}
return list
return list.unwrap()
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
@@ -59,21 +68,25 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
}
val list = getRawList()
if (!tags.isNullOrEmpty()) {
list.retainAll { x ->
x.tags.containsAll(tags)
}
list.retainAll { x -> x.containsTags(tags) }
}
when (sortOrder) {
SortOrder.ALPHABETICAL -> list.sortBy { it.title }
SortOrder.RATING -> list.sortBy { it.rating }
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
SortOrder.NEWEST,
SortOrder.UPDATED,
-> list.sortByDescending { it.createdAt }
else -> Unit
}
return list
return list.unwrap()
}
override suspend fun getDetails(manga: Manga) = when {
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) {
"Manga is not local or saved"
}
else -> getFromFile(Uri.parse(manga.url).toFile())
}
@@ -235,9 +248,9 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
private fun CoroutineScope.getFromFileAsync(
file: File,
context: CoroutineContext,
): Deferred<Manga?> = async(context) {
): Deferred<LocalManga?> = async(context) {
runInterruptible {
runCatching { getFromFile(file) }.getOrNull()
runCatchingCancellable { LocalManga(getFromFile(file), file) }.getOrNull()
}
}
@@ -283,7 +296,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
locks.unlock(id)
}
private suspend fun getRawList(): ArrayList<Manga> {
private suspend fun getRawList(): ArrayList<LocalManga> {
val files = getAllFiles()
return coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)

View File

@@ -43,7 +43,7 @@ class DirMangaImporter(
)
it.sortChaptersByName()
it.mergeWithExisting()
it.finalize()
it.finish()
it.file
}
return localMangaRepository.getFromFile(dest)

View File

@@ -23,7 +23,12 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.domain.importer.MangaImporter
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.report
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import javax.inject.Inject
@AndroidEntryPoint
@@ -48,8 +53,8 @@ class ImportService : CoroutineIntentService() {
super.onDestroy()
}
override suspend fun processIntent(intent: Intent?) {
val uris = intent?.getParcelableArrayListExtra<Uri>(EXTRA_URIS)
override suspend fun processIntent(startId: Int, intent: Intent) {
val uris = intent.getParcelableArrayListExtra<Uri>(EXTRA_URIS)
if (uris.isNullOrEmpty()) {
return
}
@@ -69,6 +74,10 @@ class ImportService : CoroutineIntentService() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
override fun onError(startId: Int, error: Throwable) {
error.report()
}
private suspend fun importImpl(uri: Uri): Manga {
val importer = importerFactory.create(uri)
return importer.import(uri)

View File

@@ -9,13 +9,15 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import javax.inject.Inject
@AndroidEntryPoint
class LocalChaptersRemoveService : CoroutineIntentService() {
@@ -33,8 +35,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
super.onDestroy()
}
override suspend fun processIntent(intent: Intent?) {
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
override suspend fun processIntent(startId: Int, intent: Intent) {
val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
startForeground()
val mangaWithChapters = localMangaRepository.getDetails(manga)
@@ -46,6 +48,21 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
override fun onError(startId: Int, error: Throwable) {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.error_occurred))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0)
.setColor(ContextCompat.getColor(this, R.color.blue_primary_dark))
.setSilent(true)
.setContentText(error.getDisplayMessage(resources))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setAutoCancel(true)
.build()
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NOTIFICATION_ID + startId, notification)
}
private fun startForeground() {
val title = getString(R.string.local_manga_processing)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View File

@@ -5,6 +5,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.viewModels
@@ -13,10 +14,11 @@ import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.addMenuProvider
class LocalListFragment : MangaListFragment() {
class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
override val viewModel by viewModels<LocalListViewModel>()
@@ -30,6 +32,14 @@ class LocalListFragment : MangaListFragment() {
ImportDialogFragment.show(childFragmentManager)
}
override fun onFilterClick(view: View?) {
super.onFilterClick(view)
val menu = PopupMenu(requireContext(), view ?: binding.recyclerView)
menu.inflate(R.menu.popup_order)
menu.setOnMenuItemClickListener(this)
menu.show()
}
override fun onScrolledToEnd() = Unit
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
@@ -53,6 +63,17 @@ class LocalListFragment : MangaListFragment() {
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val order = when (item.itemId) {
R.id.action_order_new -> SortOrder.NEWEST
R.id.action_order_abs -> SortOrder.ALPHABETICAL
R.id.action_order_rating -> SortOrder.RATING
else -> return false
}
viewModel.setSortOrder(order)
return true
}
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.delete_manga)

View File

@@ -23,4 +23,4 @@ class LocalListMenuProvider(
else -> false
}
}
}
}

View File

@@ -1,47 +1,58 @@
package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.IOException
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
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.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
@HiltViewModel
class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
settings: AppSettings,
) : MangaListViewModel(settings) {
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
) : MangaListViewModel(settings), ListExtraProvider {
val onMangaRemoved = SingleLiveEvent<Unit>()
val sortOrder = MutableLiveData(settings.localListOrder)
private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
private var refreshJob: Job? = null
override val content = combine(
mangaList,
createListModeFlow(),
sortOrder.asFlow(),
selectedTags,
listError,
) { list, mode, error ->
) { list, mode, order, tags, error ->
when {
error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
@@ -53,7 +64,11 @@ class LocalListViewModel @Inject constructor(
actionStringRes = R.string._import,
),
)
else -> list.toUi(mode)
else -> buildList(list.size + 1) {
add(createHeader(list, tags, order))
list.toUi(this, mode, this@LocalListViewModel)
}
}
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
@@ -63,14 +78,27 @@ class LocalListViewModel @Inject constructor(
watchDirectories()
}
override fun onUpdateFilter(tags: Set<MangaTag>) {
selectedTags.value = tags
onRefresh()
}
override fun onRefresh() {
launchLoadingJob(Dispatchers.Default) {
val prevJob = refreshJob
refreshJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
doRefresh()
}
}
override fun onRetry() = onRefresh()
fun setSortOrder(value: SortOrder) {
sortOrder.value = value
settings.localListOrder = value
onRefresh()
}
fun delete(ids: Set<Long>) {
launchLoadingJob {
withContext(Dispatchers.Default) {
@@ -78,7 +106,7 @@ class LocalListViewModel @Inject constructor(
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatching {
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
@@ -93,7 +121,9 @@ class LocalListViewModel @Inject constructor(
private suspend fun doRefresh() {
try {
listError.value = null
mangaList.value = repository.getList(0, null, null)
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}
@@ -102,7 +132,7 @@ class LocalListViewModel @Inject constructor(
private fun cleanup() {
if (!DownloadService.isRunning && !ImportService.isRunning && !LocalChaptersRemoveService.isRunning) {
viewModelScope.launch {
runCatching {
runCatchingCancellable {
repository.cleanup()
}.onFailure { error ->
error.printStackTraceDebug()
@@ -119,4 +149,46 @@ class LocalListViewModel @Inject constructor(
}
}
}
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
val tags = HashMap<MangaTag, Int>()
for (item in mangaList) {
for (tag in item.tags) {
tags[tag] = tags[tag]?.plus(1) ?: 1
}
}
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
val chips = LinkedList<ChipsView.ChipModel>()
for ((tag, _) in topTags) {
val model = ChipsView.ChipModel(
icon = 0,
title = tag.title,
isCheckable = true,
isChecked = tag in selectedTags,
data = tag,
)
if (model.isChecked) {
chips.addFirst(model)
} else {
chips.addLast(model)
}
}
return ListHeader2(
chips = chips,
sortOrder = order,
hasSelectedTags = selectedTags.isNotEmpty(),
)
}
override suspend fun getCounter(mangaId: Long): Int {
return trackingRepository.getNewChaptersCount(mangaId)
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
}

View File

@@ -27,13 +27,14 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.library.ui.LibraryFragment
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga
@@ -51,6 +52,7 @@ import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.*
import kotlin.text.Typography.dagger
private const val TAG_SEARCH = "search"
@@ -126,16 +128,18 @@ class MainActivity :
binding.searchView.clearFocus()
when {
fragment != null -> supportFragmentManager.commit {
setReorderingAllowed(true)
remove(fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchClosed() }
}
else -> super.onBackPressed()
}
}
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
if (fragment is LibraryFragment) {
if (fragment is ShelfFragment) {
binding.fab?.show()
} else {
binding.fab?.hide()
@@ -172,6 +176,7 @@ class MainActivity :
if (v?.id == R.id.searchView && hasFocus) {
if (fragment == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchOpened() }
@@ -289,16 +294,16 @@ class MainActivity :
}
private fun onFirstStart() {
lifecycleScope.launchWhenResumed {
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
}
requestNotificationsPermission()
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
}
}
@@ -312,7 +317,7 @@ class MainActivity :
isResumeEnabled &&
!actionModeDelegate.isActionModeStarted &&
!isSearchOpened &&
topFragment is LibraryFragment
topFragment is ShelfFragment
) {
if (fab?.isVisible == false) {
fab.show()

View File

@@ -10,7 +10,7 @@ import com.google.android.material.navigation.NavigationBarView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.explore.ui.ExploreFragment
import org.koitharu.kotatsu.library.ui.LibraryFragment
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import java.util.*
@@ -73,7 +73,7 @@ class MainNavigationDelegate(
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
setPrimaryFragment(
when (itemId) {
R.id.nav_library -> LibraryFragment.newInstance()
R.id.nav_shelf -> ShelfFragment.newInstance()
R.id.nav_explore -> ExploreFragment.newInstance()
R.id.nav_feed -> FeedFragment.newInstance()
R.id.nav_tools -> ToolsFragment.newInstance()
@@ -85,6 +85,7 @@ class MainNavigationDelegate(
private fun setPrimaryFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.setReorderingAllowed(true)
.replace(R.id.container, fragment, TAG_PRIMARY)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit()

View File

@@ -49,7 +49,10 @@ class ProtectActivity :
startActivity(intent)
finishAfterTransition()
}
}
override fun onStart() {
super.onStart()
if (!useFingerprint()) {
binding.editPassword.requestFocus()
}

View File

@@ -9,15 +9,23 @@ import android.transition.Fade
import android.transition.Slide
import android.transition.TransitionManager
import android.transition.TransitionSet
import android.view.*
import android.view.Gravity
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -42,7 +50,16 @@ import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.IdlingDetector
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
import org.koitharu.kotatsu.utils.ext.postDelayed
import org.koitharu.kotatsu.utils.ext.report
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@AndroidEntryPoint
class ReaderActivity :
@@ -145,6 +162,7 @@ class ReaderActivity :
R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this))
}
R.id.action_chapters -> {
ChaptersBottomSheet.show(
supportFragmentManager,
@@ -152,6 +170,7 @@ class ReaderActivity :
viewModel.getCurrentState()?.chapterId ?: 0L,
)
}
R.id.action_pages_thumbs -> {
val pages = viewModel.getCurrentChapterPages()
if (!pages.isNullOrEmpty()) {
@@ -165,6 +184,7 @@ class ReaderActivity :
return false
}
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value == true) {
viewModel.removeBookmark()
@@ -172,11 +192,13 @@ class ReaderActivity :
viewModel.addBookmark()
}
}
R.id.action_options -> {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val currentMode = readerManager.currentMode ?: return false
ReaderConfigBottomSheet.show(supportFragmentManager, currentMode)
}
else -> return super.onOptionsItemSelected(item)
}
return true
@@ -205,7 +227,7 @@ class ReaderActivity :
val resolveTextId = ExceptionResolver.getResolveStringId(e)
if (resolveTextId != 0) {
dialog.setPositiveButton(resolveTextId, listener)
} else {
} else if (e.isReportable()) {
dialog.setPositiveButton(R.string.report, listener)
}
dialog.show()
@@ -364,7 +386,7 @@ class ReaderActivity :
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
}
}
if (uiState.totalPages > 1 && uiState.currentPage < uiState.totalPages) {
if (uiState.isSliderAvailable()) {
binding.slider.valueTo = uiState.totalPages.toFloat() - 1
binding.slider.value = uiState.currentPage.toFloat()
binding.slider.isVisible = true
@@ -383,7 +405,7 @@ class ReaderActivity :
if (ExceptionResolver.canResolve(exception)) {
tryResolve(exception)
} else {
exception.report("ReaderActivity::onError")
exception.report()
}
} else {
onCancel(dialog)

View File

@@ -1,83 +0,0 @@
package org.koitharu.kotatsu.reader.ui
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding
import org.koitharu.kotatsu.utils.ext.withArgs
@Deprecated("Not in use")
class ReaderConfigDialog :
AlertDialogFragment<DialogReaderConfigBinding>(),
CheckableButtonGroup.OnCheckedChangeListener {
private lateinit var mode: ReaderMode
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?,
) = DialogReaderConfigBinding.inflate(inflater, container, false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mode = arguments?.getInt(ARG_MODE)
?.let { ReaderMode.valueOf(it) }
?: ReaderMode.STANDARD
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setTitle(R.string.read_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
binding.checkableGroup.onCheckedChangeListener = this
}
override fun onDismiss(dialog: DialogInterface) {
(
(parentFragment as? Callback)
?: (activity as? Callback)
)?.onReaderModeChanged(mode)
super.onDismiss(dialog)
}
override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
mode = when (checkedId) {
R.id.button_standard -> ReaderMode.STANDARD
R.id.button_webtoon -> ReaderMode.WEBTOON
R.id.button_reversed -> ReaderMode.REVERSED
else -> return
}
}
interface Callback {
fun onReaderModeChanged(mode: ReaderMode)
}
companion object {
private const val TAG = "ReaderConfigDialog"
private const val ARG_MODE = "mode"
fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigDialog().withArgs(1) {
putInt(ARG_MODE, mode.id)
}.show(fm, TAG)
}
}

View File

@@ -62,8 +62,9 @@ class ReaderInfoBarView @JvmOverloads constructor(
init {
paint.strokeWidth = context.resources.resolveDp(2f)
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start") + insetCorner
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end") + insetCorner
val fallbackInset = resources.getDimensionPixelOffset(R.dimen.reader_bar_inset_fallback)
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start", fallbackInset) + insetCorner
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end", fallbackInset) + insetCorner
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
insetLeft = if (isRtl) insetEnd else insetStart
insetRight = if (isRtl) insetStart else insetEnd
@@ -179,12 +180,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
}
}
private fun getSystemUiDimensionOffset(name: String): Int = runCatching {
private fun getSystemUiDimensionOffset(name: String, fallback: Int = 0): Int = runCatching {
val manager = context.packageManager
val resources = manager.getResourcesForApplication("com.android.systemui")
val resId = resources.getIdentifier(name, "dimen", "com.android.systemui")
resources.getDimensionPixelOffset(resId)
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(0)
}.getOrDefault(fallback)
}

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReader
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
import java.util.*
import java.util.EnumMap
class ReaderManager(
private val fragmentManager: FragmentManager,
@@ -35,11 +35,15 @@ class ReaderManager(
fun replace(newMode: ReaderMode) {
val readerClass = requireNotNull(modeMap[newMode])
fragmentManager.commit {
setReorderingAllowed(true)
replace(containerResId, readerClass, null, null)
}
}
fun replace(reader: BaseReader<*>) {
fragmentManager.commit { replace(containerResId, reader) }
fragmentManager.commit {
setReorderingAllowed(true)
replace(containerResId, reader)
}
}
}
}

View File

@@ -10,10 +10,22 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.*
import javax.inject.Provider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
@@ -21,7 +33,11 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.parsers.exception.NotFoundException
@@ -38,6 +54,9 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireValue
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.util.Date
import javax.inject.Provider
private const val BOUNDS_PAGE_OFFSET = 2
private const val PREFETCH_LIMIT = 10
@@ -115,6 +134,10 @@ class ReaderViewModel @AssistedInject constructor(
init {
loadImpl()
settings.observe()
.onEach { key ->
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
}.launchIn(viewModelScope)
}
override fun onCleared() {
@@ -326,7 +349,7 @@ class ReaderViewModel @AssistedInject constructor(
?: manga.chapters?.randomOrNull()
?: error("There are no chapters in this manga")
val pages = repo.getPages(chapter)
return runCatching {
return runCatchingCancellable {
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
@@ -347,6 +370,7 @@ class ReaderViewModel @AssistedInject constructor(
chaptersTotal = chapters.size(),
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
currentPage = state?.page ?: 0,
isSliderEnabled = settings.isReaderSliderEnabled,
)
uiState.postValue(newState)
}
@@ -381,7 +405,7 @@ class ReaderViewModel @AssistedInject constructor(
*/
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
return processLifecycleScope.launch(Dispatchers.Default) {
runCatching {
runCatchingCancellable {
addOrUpdate(
manga = manga,
chapterId = state.chapterId,

View File

@@ -37,11 +37,13 @@ abstract class BasePageHolder<B : ViewBinding>(
protected abstract fun onBind(data: ReaderPage)
fun onAttachedToWindow() {
@CallSuper
open fun onAttachedToWindow() {
delegate.onAttachedToWindow()
}
fun onDetachedFromWindow() {
@CallSuper
open fun onDetachedFromWindow() {
delegate.onDetachedFromWindow()
}

View File

@@ -3,25 +3,31 @@ package org.koitharu.kotatsu.reader.ui.pager
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.io.File
import java.io.IOException
import kotlinx.coroutines.*
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import java.io.File
import java.io.IOException
class PageHolderDelegate(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
private val callback: Callback,
private val exceptionResolver: ExceptionResolver,
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), Observer<ReaderSettings> {
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
private val scope = loader.loaderScope + Dispatchers.Main.immediate
private var state = State.EMPTY
@@ -76,7 +82,7 @@ class PageHolderDelegate(
callback.onImageShown()
}
override fun onImageLoadError(e: Exception) {
override fun onImageLoadError(e: Throwable) {
val file = this.file
error = e
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
@@ -102,6 +108,8 @@ class PageHolderDelegate(
loader.convertInPlace(file)
state = State.CONVERTED
callback.onImageReady(file.toUri())
} catch (ce: CancellationException) {
throw ce
} catch (e2: Throwable) {
e.addSuppressed(e2)
state = State.ERROR

View File

@@ -7,8 +7,13 @@ data class ReaderUiState(
val chaptersTotal: Int,
val currentPage: Int,
val totalPages: Int,
private val isSliderEnabled: Boolean,
) {
fun isSliderAvailable(): Boolean {
return isSliderEnabled && totalPages > 1 && currentPage < totalPages
}
fun computePercent(): Float {
val ppc = 1f / chaptersTotal
val chapterIndex = chapterNumber - 1

View File

@@ -32,11 +32,11 @@ class ReversedPageHolder(
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
when (settings.zoomMode) {
ZoomMode.FIT_CENTER -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
resetScaleAndCenter()
}
ZoomMode.FIT_HEIGHT -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = height / sHeight.toFloat()
setScaleAndCenter(
minScale,
@@ -44,7 +44,7 @@ class ReversedPageHolder(
)
}
ZoomMode.FIT_WIDTH -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = width / sWidth.toFloat()
setScaleAndCenter(
minScale,
@@ -52,7 +52,7 @@ class ReversedPageHolder(
)
}
ZoomMode.KEEP_START -> {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
setScaleAndCenter(
maxScale,
PointF(sWidth.toFloat(), 0f),

View File

@@ -7,8 +7,6 @@ import android.view.View
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
@@ -28,9 +26,8 @@ open class PageHolder(
View.OnClickListener {
init {
binding.ssiv.setExecutor(Dispatchers.Default.asExecutor())
binding.ssiv.setEagerLoadingEnabled(!isLowRamDevice(context))
binding.ssiv.setOnImageEventListener(delegate)
binding.ssiv.isEagerLoadingEnabled = !isLowRamDevice(context)
binding.ssiv.addOnImageEventListener(delegate)
@Suppress("LeakingThis")
bindingInfo.buttonRetry.setOnClickListener(this)
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
@@ -63,7 +60,7 @@ open class PageHolder(
}
override fun onImageReady(uri: Uri) {
binding.ssiv.setImage(ImageSource.uri(uri))
binding.ssiv.setImage(ImageSource.Uri(uri))
}
override fun onImageShowing(settings: ReaderSettings) {
@@ -74,11 +71,11 @@ open class PageHolder(
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
when (settings.zoomMode) {
ZoomMode.FIT_CENTER -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.resetScaleAndCenter()
}
ZoomMode.FIT_HEIGHT -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
binding.ssiv.setScaleAndCenter(
binding.ssiv.minScale,
@@ -86,7 +83,7 @@ open class PageHolder(
)
}
ZoomMode.FIT_WIDTH -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
binding.ssiv.setScaleAndCenter(
binding.ssiv.minScale,
@@ -94,7 +91,7 @@ open class PageHolder(
)
}
ZoomMode.KEEP_START -> {
binding.ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE)
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.setScaleAndCenter(
binding.ssiv.maxScale,
PointF(0f, 0f),

View File

@@ -5,6 +5,7 @@ import android.view.View
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
@@ -24,11 +25,12 @@ class WebtoonHolder(
View.OnClickListener {
private var scrollToRestore = 0
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
init {
binding.ssiv.setOnImageEventListener(delegate)
binding.ssiv.regionDecoderFactory = SkiaPooledImageRegionDecoder.Factory()
binding.ssiv.addOnImageEventListener(delegate)
bindingInfo.buttonRetry.setOnClickListener(this)
GoneOnInvisibleListener(bindingInfo.progressBar).attach()
}
override fun onBind(data: ReaderPage) {
@@ -40,6 +42,16 @@ class WebtoonHolder(
binding.ssiv.recycle()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
goneOnInvisibleListener.attach()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
goneOnInvisibleListener.detach()
}
override fun onLoadingStarted() {
bindingInfo.layoutError.isVisible = false
bindingInfo.progressBar.showCompat()
@@ -56,13 +68,13 @@ class WebtoonHolder(
}
override fun onImageReady(uri: Uri) {
binding.ssiv.setImage(ImageSource.uri(uri))
binding.ssiv.setImage(ImageSource.Uri(uri))
}
override fun onImageShowing(settings: ReaderSettings) {
binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter()
with(binding.ssiv) {
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = width / sWidth.toFloat()
maxScale = minScale
scrollTo(

View File

@@ -5,10 +5,7 @@ import android.graphics.PointF
import android.util.AttributeSet
import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.parsers.util.toIntUp
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.parents
private const val SCROLL_UNKNOWN = -1
@@ -23,11 +20,6 @@ class WebtoonImageView @JvmOverloads constructor(
private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN
init {
setExecutor(Dispatchers.Default.asExecutor())
setEagerLoadingEnabled(!isLowRamDevice(context))
}
fun scrollBy(delta: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
@@ -113,4 +105,4 @@ class WebtoonImageView @JvmOverloads constructor(
private fun parentHeight(): Int {
return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
}
}
}

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
fun pageThumbnailAD(
@@ -72,7 +73,7 @@ fun pageThumbnailAD(
text = (item.number).toString()
}
job = scope.launch {
val drawable = runCatching {
val drawable = runCatchingCancellable {
loadPageThumbnail(item)
}.getOrNull()
binding.imageViewThumb.setImageDrawable(drawable)

View File

@@ -5,11 +5,16 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
@@ -21,13 +26,20 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator
import org.koitharu.kotatsu.list.ui.filter.FilterItem
import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
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.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.util.LinkedList
private const val FILTER_MIN_INTERVAL = 250L
@@ -138,6 +150,8 @@ class RemoteListViewModel @AssistedInject constructor(
mangaList.value = mangaList.value?.plus(list) ?: list
}
hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
e.printStackTraceDebug()
listError.value = e

View File

@@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain
import androidx.collection.LongSparseArray
import androidx.collection.getOrElse
import androidx.core.text.parseAsHtml
import java.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.domain.model.*
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.utils.ext.findKeyByValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.util.EnumMap
abstract class Scrobbler(
protected val db: MangaDatabase,
@@ -47,7 +52,7 @@ abstract class Scrobbler(
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
val mangaInfo = infoCache.getOrElse(targetId) {
runCatching {
runCatchingCancellable {
getMangaInfo(targetId)
}.onFailure {
it.printStackTraceDebug()
@@ -72,9 +77,9 @@ abstract class Scrobbler(
}
suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean {
return runCatching {
return runCatchingCancellable {
scrobble(mangaId, chapter)
}.onFailure {
it.printStackTraceDebug()
}.isSuccess
}
}

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaSearchRepository @Inject constructor(
private val settings: AppSettings,
@@ -33,7 +34,7 @@ class MangaSearchRepository @Inject constructor(
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
settings.getMangaSources(includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source ->
runCatching {
runCatchingCancellable {
mangaRepositoryFactory.create(source).getList(
offset = 0,
query = query,

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class MangaListActivity :
@@ -41,6 +42,7 @@ class MangaListActivity :
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = if (source == MangaSource.LOCAL) {
LocalListFragment.newInstance()
} else {

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.utils.ext.showKeyboard
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
@@ -61,6 +62,7 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
}
title = query
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container, SearchFragment.newInstance(source, q))
}
binding.searchView.clearFocus()

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
@@ -12,7 +13,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
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.utils.ext.asLiveDataDistinct
@@ -47,6 +54,7 @@ class SearchViewModel @AssistedInject constructor(
actionStringRes = 0,
),
)
else -> {
val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode)
@@ -94,6 +102,8 @@ class SearchViewModel @AssistedInject constructor(
mangaList.value = mangaList.value?.plus(list) ?: list
}
hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}

View File

@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8
@@ -54,6 +55,7 @@ class MultiSearchViewModel @AssistedInject constructor(
)
},
)
loading -> list + LoadingFooter
else -> list
}
@@ -85,6 +87,8 @@ class MultiSearchViewModel @AssistedInject constructor(
loadingData.value = true
query.postValue(q)
searchImpl(q)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
} finally {
@@ -98,7 +102,7 @@ class MultiSearchViewModel @AssistedInject constructor(
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = sources.map { source ->
async(dispatcher) {
runCatching {
runCatchingCancellable {
val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID)
if (list.isNotEmpty()) {

View File

@@ -5,13 +5,13 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.*
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel
import kotlin.jvm.internal.Intrinsics
class MultiSearchAdapter(
lifecycleOwner: LifecycleOwner,
@@ -34,11 +34,11 @@ class MultiSearchAdapter(
selectionDecoration = selectionDecoration,
listener = listener,
itemClickListener = itemClickListener,
)
),
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(listener))
.addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener))
}

View File

@@ -8,7 +8,7 @@ import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
@AndroidEntryPoint
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
@@ -82,18 +83,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
clearCache(preference, CacheDir.PAGES)
true
}
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
clearCache(preference, CacheDir.THUMBS)
true
}
AppSettings.KEY_COOKIES_CLEAR -> {
clearCookies()
true
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
clearSearchHistory(preference)
true
}
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
viewLifecycleScope.launch {
trackerRepo.clearLogs()
@@ -107,6 +112,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
true
}
AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) {
launchShikimoriAuth()
@@ -115,6 +121,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
super.onPreferenceTreeClick(preference)
}
}
else -> super.onPreferenceTreeClick(preference)
}
}
@@ -127,6 +134,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
storageManager.clearCache(cache)
val size = storageManager.computeCacheSize(cache)
preference.summary = FileSize.BYTES.format(ctx, size)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
preference.summary = e.getDisplayMessage(ctx.resources)
} finally {

View File

@@ -7,7 +7,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
@@ -20,7 +19,14 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class SourceSettingsFragment : BasePreferenceFragment(0) {
@@ -66,12 +72,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
startActivity(SourceAuthActivity.newIntent(preference.context, source))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatching {
runCatchingCancellable {
preference.summary = null
withContext(Dispatchers.Default) {
requireNotNull(repository?.getAuthProvider()?.getUsername())
@@ -91,6 +98,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show()
}
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
}
error.printStackTraceDebug()

View File

@@ -11,13 +11,13 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.io.FileOutputStream
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import java.io.FileOutputStream
@AndroidEntryPoint
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@@ -91,6 +91,8 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
}
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
dismiss()
} catch (e: InterruptedException) {
throw e
} catch (e: Exception) {
onError(e)
}

View File

@@ -42,7 +42,7 @@ class OnboardDialogFragment :
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
super.onBuildDialog(builder)
.setPositiveButton(R.string.done, this)
.setCancelable(true)
.setCancelable(false)
if (isWelcome) {
builder.setTitle(R.string.welcome)
} else {

View File

@@ -91,8 +91,8 @@ class ToolsFragment :
binding.cardUpdate.root.isVisible = false
return
}
binding.cardUpdate.textPrimary.text = getString(R.string.app_update_available_s, version.name)
binding.cardUpdate.textSecondary.text = version.description
binding.cardUpdate.textSecondary.text = getString(R.string.new_version_s, version.name)
binding.cardUpdate.textChangelog.text = version.description
binding.cardUpdate.root.isVisible = true
}

View File

@@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.StringRes
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityResultContract<Uri?, Uri?>() {
@@ -30,6 +31,6 @@ class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityRes
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return intent?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
return intent?.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.domain
package org.koitharu.kotatsu.shelf.domain
import javax.inject.Inject
import kotlinx.coroutines.flow.*
@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
class LibraryRepository @Inject constructor(
class ShelfRepository @Inject constructor(
private val db: MangaDatabase,
) {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui
package org.koitharu.kotatsu.shelf.ui
import android.os.Bundle
import android.view.LayoutInflater
@@ -19,13 +19,13 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
import org.koitharu.kotatsu.databinding.FragmentShelfBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
import org.koitharu.kotatsu.library.ui.adapter.LibraryListEventListener
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -34,10 +34,10 @@ import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@AndroidEntryPoint
class LibraryFragment :
BaseFragment<FragmentLibraryBinding>(),
class ShelfFragment :
BaseFragment<FragmentShelfBinding>(),
RecyclerViewOwner,
LibraryListEventListener {
ShelfListEventListener {
@Inject
lateinit var coil: ImageLoader
@@ -45,15 +45,15 @@ class LibraryFragment :
@Inject
lateinit var settings: AppSettings
private val viewModel by viewModels<LibraryViewModel>()
private var adapter: LibraryAdapter? = null
private var selectionController: SectionedSelectionController<LibrarySectionModel>? = null
private val viewModel by viewModels<ShelfViewModel>()
private var adapter: ShelfAdapter? = null
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
override val recyclerView: RecyclerView
get() = binding.recyclerView
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentLibraryBinding {
return FragmentLibraryBinding.inflate(inflater, container, false)
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentShelfBinding {
return FragmentShelfBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -62,9 +62,9 @@ class LibraryFragment :
selectionController = SectionedSelectionController(
activity = requireActivity(),
owner = this,
callback = LibrarySelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
callback = ShelfSelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
)
adapter = LibraryAdapter(
adapter = ShelfAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
listener = this,
@@ -73,7 +73,7 @@ class LibraryFragment :
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
addMenuProvider(LibraryMenuProvider(view.context, childFragmentManager, viewModel))
addMenuProvider(ShelfMenuProvider(view.context, childFragmentManager, viewModel))
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
@@ -86,22 +86,22 @@ class LibraryFragment :
selectionController = null
}
override fun onItemClick(item: Manga, section: LibrarySectionModel, view: View) {
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
if (selectionController?.onItemClick(section, item.id) != true) {
val intent = DetailsActivity.newIntent(view.context, item)
startActivity(intent)
}
}
override fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean {
override fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean {
return selectionController?.onItemLongClick(section, item.id) ?: false
}
override fun onSectionClick(section: LibrarySectionModel, view: View) {
override fun onSectionClick(section: ShelfSectionModel, view: View) {
selectionController?.clear()
val intent = when (section) {
is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context)
is LibrarySectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context)
is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
}
startActivity(intent)
}
@@ -143,6 +143,6 @@ class LibraryFragment :
companion object {
fun newInstance() = LibraryFragment()
fun newInstance() = ShelfFragment()
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui
package org.koitharu.kotatsu.shelf.ui
import android.content.Context
import android.view.Menu
@@ -12,19 +12,19 @@ import java.util.*
import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.library.ui.config.categories.LibraryCategoriesConfigSheet
import org.koitharu.kotatsu.library.ui.config.size.LibrarySizeBottomSheet
import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfCategoriesConfigSheet
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
import org.koitharu.kotatsu.utils.ext.startOfDay
class LibraryMenuProvider(
class ShelfMenuProvider(
private val context: Context,
private val fragmentManager: FragmentManager,
private val viewModel: LibraryViewModel,
private val viewModel: ShelfViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_library, menu)
menuInflater.inflate(R.menu.opt_shelf, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
@@ -34,7 +34,7 @@ class LibraryMenuProvider(
true
}
R.id.action_grid_size -> {
LibrarySizeBottomSheet.show(fragmentManager)
ShelfSizeBottomSheet.show(fragmentManager)
true
}
R.id.action_import -> {
@@ -42,7 +42,7 @@ class LibraryMenuProvider(
true
}
R.id.action_categories -> {
LibraryCategoriesConfigSheet.show(fragmentManager)
ShelfCategoriesConfigSheet.show(fragmentManager)
true
}
else -> false

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui
package org.koitharu.kotatsu.shelf.ui
import android.content.Context
import android.view.Menu
@@ -11,33 +11,33 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.flattenTo
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
class LibrarySelectionCallback(
class ShelfSelectionCallback(
private val recyclerView: RecyclerView,
private val fragmentManager: FragmentManager,
private val viewModel: LibraryViewModel,
) : SectionedSelectionController.Callback<LibrarySectionModel> {
private val viewModel: ShelfViewModel,
) : SectionedSelectionController.Callback<ShelfSectionModel> {
private val context: Context
get() = recyclerView.context
override fun onCreateActionMode(
controller: SectionedSelectionController<LibrarySectionModel>,
controller: SectionedSelectionController<ShelfSectionModel>,
mode: ActionMode,
menu: Menu,
): Boolean {
mode.menuInflater.inflate(R.menu.mode_library, menu)
mode.menuInflater.inflate(R.menu.mode_shelf, menu)
return true
}
override fun onPrepareActionMode(
controller: SectionedSelectionController<LibrarySectionModel>,
controller: SectionedSelectionController<ShelfSectionModel>,
mode: ActionMode,
menu: Menu,
): Boolean {
@@ -47,7 +47,7 @@ class LibrarySelectionCallback(
}
override fun onActionItemClicked(
controller: SectionedSelectionController<LibrarySectionModel>,
controller: SectionedSelectionController<ShelfSectionModel>,
mode: ActionMode,
item: MenuItem,
): Boolean {
@@ -70,8 +70,8 @@ class LibrarySelectionCallback(
R.id.action_remove -> {
val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false
when (group) {
is LibrarySectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
is LibrarySectionModel.History -> viewModel.removeFromHistory(ids)
is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
is ShelfSectionModel.History -> viewModel.removeFromHistory(ids)
}
mode.finish()
true
@@ -80,18 +80,18 @@ class LibrarySelectionCallback(
}
}
override fun onSelectionChanged(controller: SectionedSelectionController<LibrarySectionModel>, count: Int) {
override fun onSelectionChanged(controller: SectionedSelectionController<ShelfSectionModel>, count: Int) {
recyclerView.invalidateNestedItemDecorations()
}
override fun onCreateItemDecoration(
controller: SectionedSelectionController<LibrarySectionModel>,
section: LibrarySectionModel,
controller: SectionedSelectionController<ShelfSectionModel>,
section: ShelfSectionModel,
): AbstractSelectionItemDecoration = MangaSelectionDecoration(context)
private fun collectSelectedItemsMap(
controller: SectionedSelectionController<LibrarySectionModel>,
): Map<LibrarySectionModel, Set<Manga>> {
controller: SectionedSelectionController<ShelfSectionModel>,
): Map<ShelfSectionModel, Set<Manga>> {
val snapshot = controller.peekCheckedIds()
if (snapshot.isEmpty()) {
return emptyMap()
@@ -100,7 +100,7 @@ class LibrarySelectionCallback(
}
private fun collectSelectedItems(
controller: SectionedSelectionController<LibrarySectionModel>,
controller: SectionedSelectionController<ShelfSectionModel>,
): Set<Manga> {
val snapshot = controller.peekCheckedIds()
if (snapshot.isEmpty()) {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui
package org.koitharu.kotatsu.shelf.ui
import androidx.collection.ArraySet
import androidx.lifecycle.LiveData
@@ -20,8 +20,8 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.library.domain.LibraryRepository
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga
@@ -33,8 +33,8 @@ import org.koitharu.kotatsu.utils.ext.daysDiff
private const val HISTORY_MAX_SEGMENTS = 2
@HiltViewModel
class LibraryViewModel @Inject constructor(
repository: LibraryRepository,
class ShelfViewModel @Inject constructor(
repository: ShelfRepository,
private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
@@ -101,7 +101,7 @@ class LibraryViewModel @Inject constructor(
val snapshot = content.value ?: return emptySet()
val result = ArraySet<Manga>(ids.size)
for (section in snapshot) {
if (section !is LibrarySectionModel) {
if (section !is ShelfSectionModel) {
continue
}
for (item in section.items) {
@@ -140,7 +140,7 @@ class LibraryViewModel @Inject constructor(
}
private suspend fun mapHistory(
destination: MutableList<in LibrarySectionModel.History>,
destination: MutableList<in ShelfSectionModel.History>,
list: List<MangaWithHistory>,
) {
val showPercent = settings.isReadingIndicatorsEnabled
@@ -151,7 +151,7 @@ class LibraryViewModel @Inject constructor(
groups[groups.keys.last()]?.addAll(subList)
}
for ((timeAgo, subList) in groups) {
destination += LibrarySectionModel.History(
destination += ShelfSectionModel.History(
items = subList.map { (manga, history) ->
val counter = trackingRepository.getNewChaptersCount(manga.id)
val percent = if (showPercent) history.percent else PROGRESS_NONE
@@ -164,12 +164,12 @@ class LibraryViewModel @Inject constructor(
}
private suspend fun mapFavourites(
destination: MutableList<in LibrarySectionModel.Favourites>,
destination: MutableList<in ShelfSectionModel.Favourites>,
favourites: Map<FavouriteCategory, List<Manga>>,
) {
for ((category, list) in favourites) {
if (list.isNotEmpty()) {
destination += LibrarySectionModel.Favourites(
destination += ShelfSectionModel.Favourites(
items = list.toUi(ListMode.GRID, this),
category = category,
showAllButtonText = R.string.show_all,

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.adapter
package org.koitharu.kotatsu.shelf.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
@@ -27,4 +27,4 @@ class MangaItemDiffCallback : DiffUtil.ItemCallback<ListModel>() {
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.adapter
package org.koitharu.kotatsu.shelf.ui.adapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.adapter
package org.koitharu.kotatsu.shelf.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
@@ -9,7 +9,7 @@ import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
@@ -17,19 +17,19 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class LibraryAdapter(
class ShelfAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
listener: LibraryListEventListener,
listener: ShelfListEventListener,
sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<LibrarySectionModel>,
selectionController: SectionedSelectionController<ShelfSectionModel>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
init {
val pool = RecyclerView.RecycledViewPool()
delegatesManager
.addDelegate(
libraryGroupAD(
shelfGroupAD(
sharedPool = pool,
lifecycleOwner = lifecycleOwner,
coil = coil,
@@ -40,12 +40,12 @@ class LibraryAdapter(
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(listener))
.addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener))
}
override fun getSectionText(context: Context, position: Int): CharSequence {
val item = items.getOrNull(position) as? LibrarySectionModel
val item = items.getOrNull(position) as? ShelfSectionModel
return item?.getTitle(context.resources) ?: ""
}
@@ -53,7 +53,7 @@ class LibraryAdapter(
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> {
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
@@ -66,7 +66,7 @@ class LibraryAdapter(
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> Unit
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.adapter
package org.koitharu.kotatsu.shelf.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -20,14 +20,14 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun libraryGroupAD(
fun shelfGroupAD(
sharedPool: RecyclerView.RecycledViewPool,
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<LibrarySectionModel>,
listener: LibraryListEventListener,
) = adapterDelegateViewBinding<LibrarySectionModel, ListModel, ItemListGroupBinding>(
selectionController: SectionedSelectionController<ShelfSectionModel>,
listener: ShelfListEventListener,
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
) {
val listenerAdapter = object : OnListItemClickListener<Manga>, View.OnClickListener {
@@ -55,11 +55,8 @@ fun libraryGroupAD(
binding.recyclerView.addItemDecoration(spacingDecoration)
binding.buttonMore.setOnClickListener(listenerAdapter)
bind { payloads ->
if (payloads.isEmpty()) {
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
selectionController.attachToRecyclerView(item, binding.recyclerView)
}
bind {
selectionController.attachToRecyclerView(item, binding.recyclerView)
binding.textViewTitle.text = item.getTitle(context.resources)
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
adapter.items = item.items

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.shelf.ui.adapter
import android.view.View
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.parsers.model.Manga
interface ShelfListEventListener : ListStateHolderListener {
fun onItemClick(item: Manga, section: ShelfSectionModel, view: View)
fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean
fun onSectionClick(section: ShelfSectionModel, view: View)
}

View File

@@ -1,16 +1,16 @@
package org.koitharu.kotatsu.library.ui.config.categories
package org.koitharu.kotatsu.shelf.ui.config.categories
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
class LibraryCategoriesConfigAdapter(
class ShelfCategoriesConfigAdapter(
listener: OnListItemClickListener<FavouriteCategory>,
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
init {
delegatesManager.addDelegate(libraryCategoryAD(listener))
delegatesManager.addDelegate(shelfCategoryAD(listener))
}
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.config.categories
package org.koitharu.kotatsu.shelf.ui.config.categories
import android.os.Bundle
import android.view.LayoutInflater
@@ -15,12 +15,12 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.SheetBaseBinding
@AndroidEntryPoint
class LibraryCategoriesConfigSheet :
class ShelfCategoriesConfigSheet :
BaseBottomSheet<SheetBaseBinding>(),
OnListItemClickListener<FavouriteCategory>,
View.OnClickListener {
private val viewModel by viewModels<LibraryCategoriesConfigViewModel>()
private val viewModel by viewModels<ShelfCategoriesConfigViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
return SheetBaseBinding.inflate(inflater, container, false)
@@ -31,7 +31,7 @@ class LibraryCategoriesConfigSheet :
binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
binding.buttonDone.isVisible = true
binding.buttonDone.setOnClickListener(this)
val adapter = LibraryCategoriesConfigAdapter(this)
val adapter = ShelfCategoriesConfigAdapter(this)
binding.recyclerView.adapter = adapter
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
@@ -47,8 +47,8 @@ class LibraryCategoriesConfigSheet :
companion object {
private const val TAG = "LibraryCategoriesConfigSheet"
private const val TAG = "ShelfCategoriesConfigSheet"
fun show(fm: FragmentManager) = LibraryCategoriesConfigSheet().show(fm, TAG)
fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.config.categories
package org.koitharu.kotatsu.shelf.ui.config.categories
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@HiltViewModel
class LibraryCategoriesConfigViewModel @Inject constructor(
class ShelfCategoriesConfigViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.config.categories
package org.koitharu.kotatsu.shelf.ui.config.categories
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
fun libraryCategoryAD(
fun shelfCategoryAD(
listener: OnListItemClickListener<FavouriteCategory>,
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.config.size
package org.koitharu.kotatsu.shelf.ui.config.size
import android.os.Bundle
import android.view.LayoutInflater
@@ -12,13 +12,13 @@ import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.SheetLibrarySizeBinding
import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding
import org.koitharu.kotatsu.utils.ext.setValueRounded
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
@AndroidEntryPoint
class LibrarySizeBottomSheet :
BaseBottomSheet<SheetLibrarySizeBinding>(),
class ShelfSizeBottomSheet :
BaseBottomSheet<SheetShelfSizeBinding>(),
Slider.OnChangeListener,
View.OnClickListener {
@@ -26,8 +26,8 @@ class LibrarySizeBottomSheet :
lateinit var settings: AppSettings
private var labelFormatter: LabelFormatter? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetLibrarySizeBinding {
return SheetLibrarySizeBinding.inflate(inflater, container, false)
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetShelfSizeBinding {
return SheetShelfSizeBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -59,8 +59,8 @@ class LibrarySizeBottomSheet :
companion object {
private const val TAG = "LibrarySizeBottomSheet"
private const val TAG = "ShelfSizeBottomSheet"
fun show(fm: FragmentManager) = LibrarySizeBottomSheet().show(fm, TAG)
fun show(fm: FragmentManager) = ShelfSizeBottomSheet().show(fm, TAG)
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.library.ui.model
package org.koitharu.kotatsu.shelf.ui.model
import android.content.res.Resources
import androidx.annotation.StringRes
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
sealed class LibrarySectionModel(
sealed class ShelfSectionModel(
val items: List<MangaItemModel>,
@StringRes val showAllButtonText: Int,
) : ListModel {
@@ -20,7 +20,7 @@ sealed class LibrarySectionModel(
items: List<MangaItemModel>,
val timeAgo: DateTimeAgo?,
showAllButtonText: Int,
) : LibrarySectionModel(items, showAllButtonText) {
) : ShelfSectionModel(items, showAllButtonText) {
override val key: Any
get() = timeAgo?.javaClass ?: this::class.java
@@ -58,7 +58,7 @@ sealed class LibrarySectionModel(
items: List<MangaItemModel>,
val category: FavouriteCategory,
showAllButtonText: Int,
) : LibrarySectionModel(items, showAllButtonText) {
) : ShelfSectionModel(items, showAllButtonText) {
override val key: Any
get() = category.id

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class SuggestionsActivity :
@@ -28,6 +29,7 @@ class SuggestionsActivity :
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = SuggestionsFragment.newInstance()
replace(R.id.container, fragment)
}

View File

@@ -11,8 +11,6 @@ import androidx.hilt.work.HiltWorker
import androidx.work.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlin.math.pow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -21,12 +19,17 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
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.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit
import kotlin.math.pow
@HiltWorker
class SuggestionsWorker @AssistedInject constructor(
@@ -94,14 +97,10 @@ class SuggestionsWorker @AssistedInject constructor(
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val rawResults = coroutineScope {
tagsBySources.flatMap { (source, tags) ->
val repo = mangaRepositoryFactory.create(source)
val repo = mangaRepositoryFactory.tryCreate(source) ?: return@flatMap emptyList()
tags.map { tag ->
async(dispatcher) {
repo.getList(
offset = 0,
sortOrder = SortOrder.UPDATED,
tags = setOf(tag),
)
repo.getListSafe(tag)
}
}
}.awaitAll().flatten().asArrayList()
@@ -139,6 +138,18 @@ class SuggestionsWorker @AssistedInject constructor(
return (weight / maxWeight).pow(2.0).toFloat()
}
private suspend fun MangaRepository.getListSafe(tag: MangaTag) = runCatchingCancellable {
getList(offset = 0, sortOrder = SortOrder.UPDATED, tags = setOf(tag))
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrDefault(emptyList())
private fun MangaRepository.Factory.tryCreate(source: MangaSource) = runCatching {
create(source)
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
companion object {
private const val TAG = "suggestions"

View File

@@ -35,4 +35,4 @@ class SyncAuthenticator(
)
}
}.getOrNull()
}
}

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
@AndroidEntryPoint
class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickListener {
@@ -33,7 +34,8 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySyncAuthBinding.inflate(layoutInflater))
accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
accountAuthenticatorResponse =
intent.getParcelableExtraCompat(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
accountAuthenticatorResponse?.onRequestContinued()
binding.buttonCancel.setOnClickListener(this)
binding.buttonNext.setOnClickListener(this)
@@ -57,6 +59,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onBackPressed() {
if (binding.switcher.isVisible && binding.switcher.displayedChild > 0) {
binding.switcher.showPrevious()

View File

@@ -9,6 +9,7 @@ import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@@ -20,9 +21,9 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
syncResult: SyncResult,
) {
val syncHelper = SyncHelper(context, account, provider)
runCatching {
runCatchingCancellable {
syncHelper.syncFavourites(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError)
}
}
}

View File

@@ -9,6 +9,7 @@ import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@@ -20,9 +21,9 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
syncResult: SyncResult,
) {
val syncHelper = SyncHelper(context, account, provider)
runCatching {
runCatchingCancellable {
syncHelper.syncHistory(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError)
}
}
}

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