Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009eb9fe44 | ||
|
|
fc8a5ccd9f | ||
|
|
91f46de547 | ||
|
|
d548993e14 | ||
|
|
4f32664b33 | ||
|
|
71b14a3aa8 | ||
|
|
183a61272e | ||
|
|
f1f208ad15 | ||
|
|
c6983d794c | ||
|
|
8228153c83 | ||
|
|
844bd13a07 | ||
|
|
60a5620134 | ||
|
|
dd09a39077 | ||
|
|
1511bd3279 | ||
|
|
259c335607 | ||
|
|
86367b6d3b | ||
|
|
19b893738d | ||
|
|
d817ae0394 | ||
|
|
d81c22b586 | ||
|
|
cd23b044df | ||
|
|
4922881343 | ||
|
|
ff0d04bea6 | ||
|
|
97de629c3b | ||
|
|
7b482e5bcf | ||
|
|
fd575b8131 | ||
|
|
c77e023bef | ||
|
|
a3cf52859b | ||
|
|
5e55bce529 | ||
|
|
b1ba70bf77 | ||
|
|
b930272221 | ||
|
|
75305c0b94 | ||
|
|
24b16e2ce2 | ||
|
|
0ccbba6787 | ||
|
|
ca314867f2 | ||
|
|
236e284360 | ||
|
|
e9a09b6be4 | ||
|
|
9e1be337ed | ||
|
|
104f2ebfae | ||
|
|
6a2e12dc29 | ||
|
|
9587cb439c | ||
|
|
c42d0824b0 | ||
|
|
09f6dd9b4e | ||
|
|
b494c96e31 | ||
|
|
0f6d56ee2d | ||
|
|
8d15691e17 | ||
|
|
bd8b251934 | ||
|
|
2f1b74e45a | ||
|
|
73217b8e11 | ||
|
|
759df969c9 | ||
|
|
466e35fffa | ||
|
|
f44db3dbff | ||
|
|
315870abcb | ||
|
|
3e46b3957c | ||
|
|
6dc81468d2 | ||
|
|
56bc0dbf07 | ||
|
|
7bc33adca8 | ||
|
|
c8794d59f7 | ||
|
|
9c2a57812e | ||
|
|
6bd5033858 | ||
|
|
e7c2a76219 | ||
|
|
0934363298 | ||
|
|
de29527805 | ||
|
|
f11e964f0b | ||
|
|
61a98f54b9 | ||
|
|
50e67daea4 | ||
|
|
0030706226 | ||
|
|
056ef5433d | ||
|
|
c14b2ceeff | ||
|
|
ff2cf9d18a | ||
|
|
96b6900c70 | ||
|
|
c6228d3fe1 | ||
|
|
8ac95e1608 | ||
|
|
69a9ec354b | ||
|
|
0639d3e6c1 | ||
|
|
ae5cebd42d | ||
|
|
cd8381cbfb | ||
|
|
3132049a63 | ||
|
|
bc3a7fc211 | ||
|
|
e794f84c6f | ||
|
|
76709dda21 | ||
|
|
6dc460bc20 | ||
|
|
c2ee548f0a | ||
|
|
1847759ec3 | ||
|
|
02d5dfb375 | ||
|
|
12d8d3e2d1 | ||
|
|
b5705b45df | ||
|
|
46b797fc67 | ||
|
|
5ec7fbed94 | ||
|
|
b48c6d7d38 | ||
|
|
da4aedca97 | ||
|
|
32695f9816 | ||
|
|
bece4cc15d | ||
|
|
548c41fbf9 | ||
|
|
ef9b16da0b | ||
|
|
5d1ef983e9 | ||
|
|
eb78a776cf | ||
|
|
661e502003 | ||
|
|
8c5c7d6b04 | ||
|
|
b1187c611a | ||
|
|
893ba37c86 | ||
|
|
b1bc94b1e9 | ||
|
|
2e3be00e26 | ||
|
|
84f41810c5 | ||
|
|
f0a4fa4e95 | ||
|
|
0c132a521e | ||
|
|
3d05541f61 | ||
|
|
2442e7cbe1 | ||
|
|
4522c478cb | ||
|
|
6881c22453 | ||
|
|
5a0c54e00f | ||
|
|
47f346b42c | ||
|
|
dc358ae6a2 | ||
|
|
bfa9feaef0 | ||
|
|
c3216871ed | ||
|
|
a8f5714b35 | ||
|
|
84567767a0 | ||
|
|
eb7efaaac9 | ||
|
|
3729b5f2f0 | ||
|
|
e4c2797f06 | ||
|
|
e02899c3f2 | ||
|
|
96c89a716e | ||
|
|
65ed5c7e6b | ||
|
|
3f96f34b8e |
3
.weblate
Normal file
3
.weblate
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[weblate]
|
||||||
|
url = https://hosted.weblate.org/api/
|
||||||
|
translation = kotatsu/strings
|
||||||
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 546
|
versionCode 555
|
||||||
versionName '5.1.2'
|
versionName '5.2.3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
|
main.java.srcDirs += 'src/main/kotlin/'
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -59,7 +60,7 @@ android {
|
|||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError true
|
abortOnError true
|
||||||
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
|
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources true
|
unitTests.includeAndroidResources true
|
||||||
@@ -78,17 +79,17 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:ebcc6391d6') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.7.1'
|
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
|
||||||
@@ -96,16 +97,17 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
||||||
|
|
||||||
|
// TODO https://issuetracker.google.com/issues/254846063
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.google.guava:guava:31.1-android') {
|
implementation('com.google.guava:guava:32.0.0-android') {
|
||||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
@@ -115,8 +117,8 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.1'
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
kapt 'androidx.room:room-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.3.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
@@ -127,8 +129,8 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.3.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
@@ -136,7 +138,7 @@ dependencies {
|
|||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation 'ch.acra:acra-http:5.9.7'
|
||||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
implementation 'ch.acra:acra-dialog:5.9.7'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20230227'
|
testImplementation 'org.json:json:20230227'
|
||||||
|
|||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -8,7 +8,7 @@
|
|||||||
public static void checkParameterIsNotNull(...);
|
public static void checkParameterIsNotNull(...);
|
||||||
public static void checkNotNullParameter(...);
|
public static void checkNotNullParameter(...);
|
||||||
}
|
}
|
||||||
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
|
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||||
-dontwarn okhttp3.internal.platform.**
|
-dontwarn okhttp3.internal.platform.**
|
||||||
-dontwarn org.conscrypt.**
|
-dontwarn org.conscrypt.**
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -19,11 +18,12 @@ import org.junit.runner.RunWith
|
|||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.awaitForIdle
|
import org.koitharu.kotatsu.awaitForIdle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ShortcutsUpdaterTest {
|
class AppShortcutManagerTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var hiltRule = HiltAndroidRule(this)
|
var hiltRule = HiltAndroidRule(this)
|
||||||
@@ -32,7 +32,7 @@ class ShortcutsUpdaterTest {
|
|||||||
lateinit var historyRepository: HistoryRepository
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var appShortcutManager: AppShortcutManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: MangaDatabase
|
lateinit var database: MangaDatabase
|
||||||
@@ -72,6 +72,6 @@ class ShortcutsUpdaterTest {
|
|||||||
private suspend fun awaitUpdate() {
|
private suspend fun awaitUpdate() {
|
||||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||||
instrumentation.awaitForIdle()
|
instrumentation.awaitForIdle()
|
||||||
shortcutsUpdater.await()
|
appShortcutManager.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.tracker.domain
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import javax.inject.Inject
|
|
||||||
import junit.framework.TestCase.*
|
import junit.framework.TestCase.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -11,8 +10,9 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import java.util.EnumSet
|
|||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("", null)
|
get() = ConfigKey.Domain()
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
|
|
||||||
|
class LoggingAdapterDataObserver(
|
||||||
|
private val tag: String,
|
||||||
|
) : AdapterDataObserver() {
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
Log.d(tag, "onChanged()")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateRestorationPolicyChanged() {
|
||||||
|
Log.d(tag, "onStateRestorationPolicyChanged()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
@@ -102,6 +102,10 @@
|
|||||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
@@ -184,8 +188,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
|
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/favourites"
|
android:label="@string/favourites">
|
||||||
android:process=":sync">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -196,8 +199,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
|
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/history"
|
android:label="@string/history">
|
||||||
android:process=":sync">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
abstract class BaseFragment<B : ViewBinding> :
|
|
||||||
Fragment(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
protected val actionModeDelegate: ActionModeDelegate
|
|
||||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val binding = onInflateView(inflater, container)
|
|
||||||
viewBinding = binding
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
insetsDelegate.onViewCreated(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
insetsDelegate.onDestroyView()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun bindingOrNull() = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
|
|
||||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
|
||||||
BaseActivity<B>(),
|
|
||||||
View.OnSystemUiVisibilityChangeListener {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
with(window) {
|
|
||||||
statusBarColor = Color.TRANSPARENT
|
|
||||||
navigationBarColor = Color.TRANSPARENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
|
||||||
}
|
|
||||||
showSystemUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
|
||||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO WindowInsetsControllerCompat works incorrect
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun hideSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun showSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService()
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class ScrollListenerInvalidationObserver(
|
|
||||||
private val recyclerView: RecyclerView,
|
|
||||||
private val scrollListener: RecyclerView.OnScrollListener,
|
|
||||||
) : RecyclerView.AdapterDataObserver() {
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
super.onChanged()
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateScroll() {
|
|
||||||
recyclerView.post {
|
|
||||||
scrollListener.onScrolled(recyclerView, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class CountedBooleanLiveData : LiveData<Boolean>(false) {
|
|
||||||
|
|
||||||
private val counter = AtomicInteger(0)
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun increment() {
|
|
||||||
if (counter.getAndIncrement() == 0) {
|
|
||||||
postValue(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun decrement() {
|
|
||||||
if (counter.decrementAndGet() == 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun reset() {
|
|
||||||
if (counter.getAndSet(0) != 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.fragment.app.setFragmentResult
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import okhttp3.Headers
|
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
|
||||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
|
||||||
|
|
||||||
private lateinit var url: String
|
|
||||||
private val pendingResult = Bundle(1)
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var cookieJar: MutableCookieJar
|
|
||||||
|
|
||||||
private var onBackPressedCallback: WebViewBackPressedCallback? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
url = requireArguments().getString(ARG_URL).orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
with(binding.webView.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome
|
|
||||||
}
|
|
||||||
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
|
||||||
if (url.isEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
} else {
|
|
||||||
binding.webView.loadUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
binding.webView.stopLoading()
|
|
||||||
binding.webView.destroy()
|
|
||||||
onBackPressedCallback = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
|
||||||
return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogCreated(dialog: AlertDialog) {
|
|
||||||
super.onDialogCreated(dialog)
|
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also {
|
|
||||||
dialog.onBackPressedDispatcher.addCallback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
binding.webView.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
binding.webView.onPause()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
setFragmentResult(TAG, pendingResult)
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageLoaded() {
|
|
||||||
bindingOrNull()?.progressBar?.isInvisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckPassed() {
|
|
||||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHistoryChanged() {
|
|
||||||
onBackPressedCallback?.onHistoryChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TAG = "CloudFlareDialog"
|
|
||||||
const val EXTRA_RESULT = "result"
|
|
||||||
private const val ARG_URL = "url"
|
|
||||||
private const val ARG_UA = "ua"
|
|
||||||
|
|
||||||
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
|
|
||||||
putString(ARG_URL, url)
|
|
||||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
|
||||||
putString(ARG_UA, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import dagger.Reusable
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@Reusable
|
|
||||||
class MangaTagHighlighter @Inject constructor(
|
|
||||||
@ApplicationContext context: Context,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dict by lazy {
|
|
||||||
context.resources.openRawResource(R.raw.tags_redlist).use {
|
|
||||||
val set = HashSet<String>()
|
|
||||||
it.bufferedReader().forEachLine { x ->
|
|
||||||
val line = x.trim()
|
|
||||||
if (line.isNotEmpty()) {
|
|
||||||
set.add(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorRes
|
|
||||||
fun getTint(tag: MangaTag): Int {
|
|
||||||
return if (tag.title.lowercase() in dict) {
|
|
||||||
R.color.warning
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
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
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ViewModelScoped
|
|
||||||
class MangaDetailsDelegate @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
) {
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
|
||||||
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
// Remote manga for saved and saved for remote
|
|
||||||
val relatedManga = MutableStateFlow<Manga?>(null)
|
|
||||||
val manga: StateFlow<Manga?>
|
|
||||||
get() = mangaData
|
|
||||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
|
||||||
|
|
||||||
suspend fun doLoad() {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
|
||||||
mangaData.value = manga
|
|
||||||
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
mangaData.value = manga
|
|
||||||
relatedManga.value = runCatchingCancellable {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
|
||||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)?.manga
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapChapters(
|
|
||||||
manga: Manga?,
|
|
||||||
related: Manga?,
|
|
||||||
history: MangaHistory?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chapters = manga?.chapters ?: return emptyList()
|
|
||||||
val relatedChapters = related?.chapters
|
|
||||||
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
||||||
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
} else {
|
|
||||||
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChapters(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
downloadedChapters: List<MangaChapter>?,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val result = ArrayList<ChapterListItem>(chapters.size)
|
|
||||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = chapters.size - newCount
|
|
||||||
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
|
|
||||||
for (i in chapters.indices) {
|
|
||||||
val chapter = chapters[i]
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (result.size < chapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChaptersWithSource(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
sourceChapters: List<MangaChapter>,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
|
||||||
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
|
||||||
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = sourceChapters.size - newCount
|
|
||||||
for (i in sourceChapters.indices) {
|
|
||||||
val chapter = sourceChapters[i]
|
|
||||||
val localChapter = chaptersMap.remove(chapter.id)
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += localChapter?.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
) ?: chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = true,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
|
||||||
result.ensureCapacity(result.size + chaptersMap.size)
|
|
||||||
chaptersMap.values.mapNotNullTo(result) {
|
|
||||||
if (it.branch == branch) {
|
|
||||||
it.toListItem(
|
|
||||||
isCurrent = false,
|
|
||||||
isUnread = true,
|
|
||||||
isNew = false,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.sortBy { it.chapter.number }
|
|
||||||
}
|
|
||||||
if (result.size < sourceChapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
|
||||||
|
|
||||||
fun chapterListItemAD(
|
|
||||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
|
||||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
|
|
||||||
itemView.setOnClickListener(eventListener)
|
|
||||||
itemView.setOnLongClickListener(eventListener)
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
binding.textViewTitle.text = item.chapter.name
|
|
||||||
binding.textViewNumber.text = item.chapter.number.toString()
|
|
||||||
binding.textViewDescription.textAndVisible = item.description()
|
|
||||||
}
|
|
||||||
when (item.status) {
|
|
||||||
FLAG_UNREAD -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
|
|
||||||
}
|
|
||||||
FLAG_CURRENT -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val isMissing = item.hasFlag(FLAG_MISSING)
|
|
||||||
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
|
|
||||||
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
|
|
||||||
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
|
||||||
|
|
||||||
class MangaCategoriesAdapter(
|
|
||||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
|
||||||
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Boolean = oldItem.id == newItem.id
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Boolean = oldItem == newItem
|
|
||||||
|
|
||||||
override fun getChangePayload(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Any? {
|
|
||||||
if (oldItem.isChecked != newItem.isChecked) {
|
|
||||||
return newItem.isChecked
|
|
||||||
}
|
|
||||||
return super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.domain
|
|
||||||
|
|
||||||
interface ListExtraProvider {
|
|
||||||
|
|
||||||
suspend fun getCounter(mangaId: Long): Int
|
|
||||||
|
|
||||||
suspend fun getProgress(mangaId: Long): Float
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
|
|
||||||
class FilterAdapter(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
listListener: ListListener<FilterItem>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
|
||||||
FilterDiffCallback(),
|
|
||||||
filterSortDelegate(listener),
|
|
||||||
filterTagDelegate(listener),
|
|
||||||
filterHeaderDelegate(),
|
|
||||||
filterLoadingDelegate(),
|
|
||||||
filterErrorDelegate(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
differ.addListListener(listListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.titleRes
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
|
||||||
|
|
||||||
fun filterSortDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onSortItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.setText(item.order.titleRes)
|
|
||||||
binding.root.isChecked = item.isSelected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterTagDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onTagItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.text = item.tag.title
|
|
||||||
binding.root.isChecked = item.isChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.setText(item.titleResId)
|
|
||||||
binding.badge.isVisible = if (item.counter == 0) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
binding.badge.text = item.counter.toString()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
|
|
||||||
|
|
||||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
(itemView as TextView).setText(item.textResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
|
||||||
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
|
|
||||||
|
|
||||||
class FilterBottomSheet :
|
|
||||||
BaseBottomSheet<SheetFilterBinding>(),
|
|
||||||
MenuItem.OnActionExpandListener,
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
AsyncListDiffer.ListListener<FilterItem> {
|
|
||||||
|
|
||||||
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
|
|
||||||
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
|
||||||
return SheetFilterBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
val adapter = FilterAdapter(viewModel, this)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
|
||||||
initOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
collapsibleActionViewCallback = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
|
||||||
setExpanded(isExpanded = true, isLocked = true)
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
|
||||||
val searchView = (item.actionView as? SearchView) ?: return false
|
|
||||||
searchView.setQuery("", false)
|
|
||||||
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
viewModel.filterSearch(newText?.trim().orEmpty())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
|
|
||||||
if (currentList.size > previousList.size && view != null) {
|
|
||||||
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOptionsMenu() {
|
|
||||||
binding.headerBar.inflateMenu(R.menu.opt_filter)
|
|
||||||
val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search)
|
|
||||||
searchMenuItem.setOnActionExpandListener(this)
|
|
||||||
val searchView = searchMenuItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this)
|
|
||||||
searchView.setIconifiedByDefault(false)
|
|
||||||
searchView.queryHint = searchMenuItem.title
|
|
||||||
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
|
|
||||||
onBackPressedDispatcher.addCallback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "FilterBottomSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
|
|
||||||
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem === newItem -> true
|
|
||||||
oldItem.javaClass != newItem.javaClass -> false
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.titleResId == newItem.titleResId
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.tag == newItem.tag
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.order == newItem.order
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
|
||||||
oldItem.textResId == newItem.textResId
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter == newItem.counter
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked == newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected == newItem.isSelected
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
|
|
||||||
val hasPayload = when {
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked != newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected != newItem.isSelected
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter != newItem.counter
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
|
|
||||||
sealed interface FilterItem {
|
|
||||||
|
|
||||||
class Header(
|
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
val counter: Int,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Sort(
|
|
||||||
val order: SortOrder,
|
|
||||||
val isSelected: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Tag(
|
|
||||||
val tag: MangaTag,
|
|
||||||
val isChecked: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
object Loading : FilterItem
|
|
||||||
|
|
||||||
class Error(
|
|
||||||
@StringRes val textResId: Int,
|
|
||||||
) : FilterItem
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
object LoadingFooter : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
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 kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
|
||||||
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.ListHeader2
|
|
||||||
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.local.data.LocalManga
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|
||||||
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.asFlowLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.LinkedList
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class LocalListViewModel @Inject constructor(
|
|
||||||
private val repository: LocalMangaRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val tagHighlighter: MangaTagHighlighter,
|
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
|
||||||
) : MangaListViewModel(settings, downloadScheduler), 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,
|
|
||||||
listModeFlow,
|
|
||||||
sortOrder.asFlow(),
|
|
||||||
selectedTags,
|
|
||||||
listError,
|
|
||||||
) { list, mode, order, tags, error ->
|
|
||||||
when {
|
|
||||||
error != null -> listOf(error.toErrorState(canRetry = true))
|
|
||||||
list == null -> listOf(LoadingState)
|
|
||||||
list.isEmpty() -> listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_empty_local,
|
|
||||||
textPrimary = R.string.text_local_holder_primary,
|
|
||||||
textSecondary = R.string.text_local_holder_secondary,
|
|
||||||
actionStringRes = R.string._import,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> buildList(list.size + 1) {
|
|
||||||
add(createHeader(list, tags, order))
|
|
||||||
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
|
||||||
|
|
||||||
init {
|
|
||||||
onRefresh()
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
localStorageChanges
|
|
||||||
.collectLatest {
|
|
||||||
if (refreshJob?.isActive != true) {
|
|
||||||
doRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateFilter(tags: Set<MangaTag>) {
|
|
||||||
selectedTags.value = tags
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
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(Dispatchers.Default) {
|
|
||||||
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
|
||||||
for (manga in itemsToRemove) {
|
|
||||||
val original = repository.getRemoteManga(manga)
|
|
||||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
|
||||||
runCatchingCancellable {
|
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
|
||||||
}
|
|
||||||
mangaList.update { list ->
|
|
||||||
list?.filterNot { it.id == manga.id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onMangaRemoved.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun doRefresh() {
|
|
||||||
try {
|
|
||||||
listError.value = null
|
|
||||||
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
listError.value = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
tint = 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 if (settings.isTrackerEnabled) {
|
|
||||||
trackingRepository.getNewChaptersCount(mangaId)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getProgress(mangaId: Long): Float {
|
|
||||||
return if (settings.isReadingIndicatorsEnabled) {
|
|
||||||
historyRepository.getProgress(mangaId)
|
|
||||||
} else {
|
|
||||||
PROGRESS_NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.main.ui.owners
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
|
|
||||||
interface NoModalBottomSheetOwner {
|
|
||||||
|
|
||||||
val bsHeader: BottomSheetHeaderBar?
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.colorfilter
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ColorFilterConfigViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
|
|
||||||
|
|
||||||
private var initialColorFilter: ReaderColorFilter? = null
|
|
||||||
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
|
|
||||||
val onDismiss = SingleLiveEvent<Unit>()
|
|
||||||
val preview = MutableLiveData<MangaPage?>(null)
|
|
||||||
|
|
||||||
val isChanged: Boolean
|
|
||||||
get() = colorFilter.value != initialColorFilter
|
|
||||||
|
|
||||||
init {
|
|
||||||
val page = checkNotNull(
|
|
||||||
savedStateHandle.get<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(),
|
|
||||||
)
|
|
||||||
launchLoadingJob {
|
|
||||||
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
|
|
||||||
colorFilter.value = initialColorFilter
|
|
||||||
}
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val repository = mangaRepositoryFactory.create(page.source)
|
|
||||||
val url = repository.getPageUrl(page)
|
|
||||||
preview.emitValue(
|
|
||||||
MangaPage(
|
|
||||||
id = page.id,
|
|
||||||
url = url,
|
|
||||||
preview = page.preview,
|
|
||||||
source = page.source,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBrightness(brightness: Float) {
|
|
||||||
val cf = colorFilter.value
|
|
||||||
colorFilter.value = ReaderColorFilter(brightness, cf?.contrast ?: 0f).takeUnless { it.isEmpty }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContrast(contrast: Float) {
|
|
||||||
val cf = colorFilter.value
|
|
||||||
colorFilter.value = ReaderColorFilter(cf?.brightness ?: 0f, contrast).takeUnless { it.isEmpty }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
colorFilter.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save() {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
|
||||||
onDismiss.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
fun interface OnPageSelectListener {
|
|
||||||
|
|
||||||
fun onPageSelected(page: MangaPage)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
data class PageThumbnail(
|
|
||||||
val number: Int,
|
|
||||||
val isCurrent: Boolean,
|
|
||||||
val repository: MangaRepository,
|
|
||||||
val page: MangaPage
|
|
||||||
)
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import coil.ImageLoader
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PagesThumbnailsSheet :
|
|
||||||
BaseBottomSheet<SheetPagesBinding>(),
|
|
||||||
OnListItemClickListener<MangaPage>,
|
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settings: AppSettings
|
|
||||||
|
|
||||||
private lateinit var thumbnails: List<PageThumbnail>
|
|
||||||
private var spanResolver: MangaListSpanResolver? = null
|
|
||||||
private var currentPageIndex = -1
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
|
|
||||||
if (pages.isNullOrEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
|
|
||||||
val repository = mangaRepositoryFactory.create(pages.first().source)
|
|
||||||
thumbnails = pages.mapIndexed { i, x ->
|
|
||||||
PageThumbnail(
|
|
||||||
number = i + 1,
|
|
||||||
isCurrent = i == currentPageIndex,
|
|
||||||
repository = repository,
|
|
||||||
page = x,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
|
||||||
return SheetPagesBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
spanResolver = MangaListSpanResolver(view.resources)
|
|
||||||
with(binding.headerBar) {
|
|
||||||
title = arguments?.getString(ARG_TITLE)
|
|
||||||
subtitle = null
|
|
||||||
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.recyclerView) {
|
|
||||||
addItemDecoration(
|
|
||||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
|
||||||
)
|
|
||||||
adapter = PageThumbnailAdapter(
|
|
||||||
dataSet = thumbnails,
|
|
||||||
coil = coil,
|
|
||||||
scope = viewLifecycleScope,
|
|
||||||
loader = pageLoader,
|
|
||||||
clickListener = this@PagesThumbnailsSheet,
|
|
||||||
)
|
|
||||||
addOnLayoutChangeListener(spanResolver)
|
|
||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
|
||||||
if (currentPageIndex > 0) {
|
|
||||||
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
|
|
||||||
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
spanResolver = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: MangaPage, view: View) {
|
|
||||||
(
|
|
||||||
(parentFragment as? OnPageSelectListener)
|
|
||||||
?: (activity as? OnPageSelectListener)
|
|
||||||
)?.run {
|
|
||||||
onPageSelected(item)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
|
||||||
if (isExpanded) {
|
|
||||||
headerBar.subtitle = resources.getQuantityString(
|
|
||||||
R.plurals.pages,
|
|
||||||
thumbnails.size,
|
|
||||||
thumbnails.size,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
headerBar.subtitle = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ARG_PAGES = "pages"
|
|
||||||
private const val ARG_TITLE = "title"
|
|
||||||
private const val ARG_CURRENT = "current"
|
|
||||||
|
|
||||||
private const val TAG = "PagesThumbnailsSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
|
|
||||||
PagesThumbnailsSheet().withArgs(3) {
|
|
||||||
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
|
|
||||||
putString(ARG_TITLE, title)
|
|
||||||
putInt(ARG_CURRENT, currentPage)
|
|
||||||
}.show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
import coil.size.Size
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
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.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
fun pageThumbnailAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>,
|
|
||||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
|
||||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
|
|
||||||
) {
|
|
||||||
var job: Job? = null
|
|
||||||
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
|
||||||
val thumbSize = Size(
|
|
||||||
width = gridWidth,
|
|
||||||
height = (gridWidth / 13f * 18f).toInt(),
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
|
|
||||||
item.page.preview?.let { url ->
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(url)
|
|
||||||
.tag(item.page.source)
|
|
||||||
.size(thumbSize)
|
|
||||||
.scale(Scale.FILL)
|
|
||||||
.allowRgb565(true)
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}?.let { drawable ->
|
|
||||||
return@withContext drawable
|
|
||||||
}
|
|
||||||
val file = loader.loadPage(item.page, force = false)
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(file)
|
|
||||||
.size(thumbSize)
|
|
||||||
.decodeRegion(0)
|
|
||||||
.allowRgb565(isLowRamDevice(context))
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
clickListener.onItemClick(item.page, itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
job?.cancel()
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
with(binding.textViewNumber) {
|
|
||||||
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
|
|
||||||
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
|
|
||||||
text = (item.number).toString()
|
|
||||||
}
|
|
||||||
job = scope.launch {
|
|
||||||
val drawable = runCatchingCancellable {
|
|
||||||
loadPageThumbnail(item)
|
|
||||||
}.getOrNull()
|
|
||||||
binding.imageViewThumb.setImageDrawable(drawable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
|
|
||||||
class PageThumbnailAdapter(
|
|
||||||
dataSet: List<PageThumbnail>,
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>
|
|
||||||
) : ListDelegationAdapter<List<PageThumbnail>>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
|
|
||||||
setItems(dataSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.search.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
|
||||||
import org.koitharu.kotatsu.local.ui.LocalListFragment
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
|
||||||
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 org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MangaListActivity :
|
|
||||||
BaseActivity<ActivityContainerBinding>(),
|
|
||||||
AppBarOwner {
|
|
||||||
|
|
||||||
override val appBar: AppBarLayout
|
|
||||||
get() = binding.appbar
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivityContainerBinding.inflate(layoutInflater))
|
|
||||||
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source
|
|
||||||
if (source == null) {
|
|
||||||
finishAfterTransition()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title
|
|
||||||
val fm = supportFragmentManager
|
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
|
||||||
fm.commit {
|
|
||||||
setReorderingAllowed(true)
|
|
||||||
val fragment = if (source == MangaSource.LOCAL) {
|
|
||||||
LocalListFragment.newInstance()
|
|
||||||
} else {
|
|
||||||
RemoteListFragment.newInstance(source)
|
|
||||||
}
|
|
||||||
replace(R.id.container, fragment)
|
|
||||||
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) {
|
|
||||||
runOnCommit(ApplyFilterRunnable(fragment, tags))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.root.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ApplyFilterRunnable(
|
|
||||||
private val fragment: RemoteListFragment,
|
|
||||||
private val tags: Set<MangaTag>,
|
|
||||||
) : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
fragment.viewModel.applyFilter(tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val EXTRA_TAGS = "tags"
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
|
||||||
|
|
||||||
fun newIntent(context: Context, tags: Set<MangaTag>) = Intent(context, MangaListActivity::class.java)
|
|
||||||
.putExtra(EXTRA_TAGS, ParcelableMangaTags(tags))
|
|
||||||
|
|
||||||
fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java)
|
|
||||||
.putExtra(EXTRA_SOURCE, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
|
|
||||||
class RootSettingsFragment : BasePreferenceFragment(R.string.settings) {
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentTransaction
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.PreferenceHeaderFragmentCompat
|
|
||||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener {
|
|
||||||
|
|
||||||
private var currentTitle: CharSequence? = null
|
|
||||||
|
|
||||||
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment()
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
slidingPaneLayout.addPanelSlideListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
|
|
||||||
|
|
||||||
override fun onPanelOpened(panel: View) {
|
|
||||||
activity?.title = currentTitle ?: getString(R.string.settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelClosed(panel: View) {
|
|
||||||
activity?.setTitle(R.string.settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(title: CharSequence?) {
|
|
||||||
currentTitle = title
|
|
||||||
if (slidingPaneLayout.width != 0 && slidingPaneLayout.isOpen) {
|
|
||||||
activity?.title = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openFragment(fragment: Fragment) {
|
|
||||||
childFragmentManager.commit {
|
|
||||||
setReorderingAllowed(true)
|
|
||||||
replace(androidx.preference.R.id.preferences_detail, fragment)
|
|
||||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
|
||||||
addToBackStack(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
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.awaitViewLifecycle
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.requireSerializable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SourceSettingsFragment : BasePreferenceFragment(0) {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
|
||||||
|
|
||||||
private lateinit var source: MangaSource
|
|
||||||
private var repository: RemoteMangaRepository? = null
|
|
||||||
private val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
source = requireArguments().requireSerializable(EXTRA_SOURCE)
|
|
||||||
repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
setTitle(source.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
preferenceManager.sharedPreferencesName = source.name
|
|
||||||
val repo = repository ?: return
|
|
||||||
addPreferencesFromResource(R.xml.pref_source)
|
|
||||||
addPreferencesFromRepository(repo)
|
|
||||||
|
|
||||||
findPreference<Preference>(KEY_AUTH)?.run {
|
|
||||||
val authProvider = repo.getAuthProvider()
|
|
||||||
isVisible = authProvider != null
|
|
||||||
isEnabled = authProvider?.isAuthorized == false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
findPreference<Preference>(KEY_AUTH)?.run {
|
|
||||||
if (isVisible) {
|
|
||||||
loadUsername(viewLifecycleOwner, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
return when (preference.key) {
|
|
||||||
KEY_AUTH -> {
|
|
||||||
startActivity(SourceAuthActivity.newIntent(preference.context, source))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
|
|
||||||
runCatchingCancellable {
|
|
||||||
preference.summary = null
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
requireNotNull(repository?.getAuthProvider()?.getUsername())
|
|
||||||
}
|
|
||||||
}.onSuccess { username ->
|
|
||||||
preference.title = getString(R.string.logged_in_as, username)
|
|
||||||
}.onFailure { error ->
|
|
||||||
when {
|
|
||||||
error is AuthRequiredException -> Unit
|
|
||||||
ExceptionResolver.canResolve(error) -> {
|
|
||||||
ensureActive()
|
|
||||||
Snackbar.make(
|
|
||||||
listView ?: return@onFailure,
|
|
||||||
error.getDisplayMessage(preference.context.resources),
|
|
||||||
Snackbar.LENGTH_INDEFINITE,
|
|
||||||
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
|
|
||||||
}
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveError(error: Throwable) {
|
|
||||||
view ?: return
|
|
||||||
viewLifecycleScope.launch {
|
|
||||||
if (exceptionResolver.resolve(error)) {
|
|
||||||
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch
|
|
||||||
val lifecycleOwner = awaitViewLifecycle()
|
|
||||||
loadUsername(lifecycleOwner, pref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_AUTH = "auth"
|
|
||||||
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
|
||||||
|
|
||||||
fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) {
|
|
||||||
putSerializable(EXTRA_SOURCE, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings.backup
|
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
class BackupSettingsFragment :
|
|
||||||
BasePreferenceFragment(R.string.backup_restore),
|
|
||||||
ActivityResultCallback<Uri?> {
|
|
||||||
|
|
||||||
private val backupSelectCall = registerForActivityResult(
|
|
||||||
ActivityResultContracts.OpenDocument(),
|
|
||||||
this
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_backup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
return when (preference.key) {
|
|
||||||
AppSettings.KEY_BACKUP -> {
|
|
||||||
BackupDialogFragment().show(childFragmentManager, BackupDialogFragment.TAG)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
AppSettings.KEY_RESTORE -> {
|
|
||||||
try {
|
|
||||||
backupSelectCall.launch(arrayOf("*/*"))
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
e.printStackTraceDebug()
|
|
||||||
Snackbar.make(
|
|
||||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(result: Uri?) {
|
|
||||||
RestoreDialogFragment.newInstance(result ?: return)
|
|
||||||
.show(childFragmentManager, BackupDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
private const val DEFAULT_TIMEOUT = 5_000L
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar to a CoroutineLiveData but optimized for using within infinite flows
|
|
||||||
*/
|
|
||||||
class FlowLiveData<T>(
|
|
||||||
private val flow: Flow<T>,
|
|
||||||
defaultValue: T,
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
private val timeoutInMs: Long = DEFAULT_TIMEOUT,
|
|
||||||
) : LiveData<T>(defaultValue) {
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Main.immediate + context + SupervisorJob(context[Job]))
|
|
||||||
private var job: Job? = null
|
|
||||||
private var cancellationJob: Job? = null
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
super.onActive()
|
|
||||||
cancellationJob?.cancel()
|
|
||||||
cancellationJob = null
|
|
||||||
if (job?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
job = scope.launch {
|
|
||||||
flow.collect(Collector())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
super.onInactive()
|
|
||||||
cancellationJob?.cancel()
|
|
||||||
cancellationJob = scope.launch(Dispatchers.Main.immediate) {
|
|
||||||
delay(timeoutInMs)
|
|
||||||
if (!hasActiveObservers()) {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class Collector : FlowCollector<T> {
|
|
||||||
|
|
||||||
private var previousValue: Any? = value
|
|
||||||
private val dispatcher = Dispatchers.Main.immediate
|
|
||||||
|
|
||||||
override suspend fun emit(value: T) {
|
|
||||||
if (previousValue != value) {
|
|
||||||
previousValue = value
|
|
||||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
|
||||||
withContext(dispatcher) {
|
|
||||||
setValue(value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setValue(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> Flow<T>.asFlowLiveData(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
defaultValue: T,
|
|
||||||
timeoutInMs: Long = DEFAULT_TIMEOUT,
|
|
||||||
): LiveData<T> = FlowLiveData(this, defaultValue, context, timeoutInMs)
|
|
||||||
|
|
||||||
fun <T> StateFlow<T>.asFlowLiveData(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
timeoutInMs: Long = DEFAULT_TIMEOUT,
|
|
||||||
): LiveData<T> = FlowLiveData(this, value, context, timeoutInMs)
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
class SingleLiveEvent<T> : LiveData<T>() {
|
|
||||||
|
|
||||||
private val pending = AtomicBoolean(false)
|
|
||||||
|
|
||||||
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
|
|
||||||
super.observe(owner) {
|
|
||||||
if (pending.compareAndSet(true, false)) {
|
|
||||||
observer.onChanged(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setValue(value: T) {
|
|
||||||
pending.set(true)
|
|
||||||
super.setValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun call(newValue: T) {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun postCall(newValue: T) {
|
|
||||||
postValue(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun emitCall(newValue: T) {
|
|
||||||
val dispatcher = Dispatchers.Main.immediate
|
|
||||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
|
||||||
withContext(dispatcher) {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
|
|
||||||
class TaggedActivityResult(
|
|
||||||
val tag: String,
|
|
||||||
val result: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
val TaggedActivityResult.isSuccess: Boolean
|
|
||||||
get() = this.result == Activity.RESULT_OK
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
|
||||||
|
|
||||||
internal val RecyclerView.LayoutManager?.firstVisibleItemPosition
|
|
||||||
get() = when (this) {
|
|
||||||
is LinearLayoutManager -> findFirstVisibleItemPosition()
|
|
||||||
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0]
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val RecyclerView.LayoutManager?.isLayoutReversed
|
|
||||||
get() = when (this) {
|
|
||||||
is LinearLayoutManager -> reverseLayout
|
|
||||||
is StaggeredGridLayoutManager -> reverseLayout
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.utils.BufferedObserver
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
|
|
||||||
"LiveData value is null"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver<T>) {
|
|
||||||
var previous: T? = null
|
|
||||||
this.observe(owner) {
|
|
||||||
observer.onChanged(it, previous)
|
|
||||||
previous = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> MutableLiveData<T>.emitValue(newValue: T) {
|
|
||||||
val dispatcher = Dispatchers.Main.immediate
|
|
||||||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
|
|
||||||
withContext(dispatcher) {
|
|
||||||
value = newValue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
|
||||||
if (obj == null || !isInstance(obj)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return obj as T
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import androidx.work.Configuration
|
|||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.acra.ACRA
|
||||||
import org.acra.ReportField
|
import org.acra.ReportField
|
||||||
import org.acra.config.dialog
|
import org.acra.config.dialog
|
||||||
import org.acra.config.httpSender
|
import org.acra.config.httpSender
|
||||||
@@ -19,13 +20,15 @@ import org.acra.data.StringFormat
|
|||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.os.AppValidator
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.utils.WorkServiceStopHelper
|
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -46,8 +49,15 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var workerFactory: HiltWorkerFactory
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appValidator: AppValidator
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workScheduleManager: WorkScheduleManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
enableStrictMode()
|
enableStrictMode()
|
||||||
}
|
}
|
||||||
@@ -57,6 +67,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
setupDatabaseObservers()
|
setupDatabaseObservers()
|
||||||
}
|
}
|
||||||
|
workScheduleManager.init()
|
||||||
WorkServiceStopHelper(applicationContext).setup()
|
WorkServiceStopHelper(applicationContext).setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +78,10 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
excludeMatchingSharedPreferencesKeys = listOf(
|
excludeMatchingSharedPreferencesKeys = listOf(
|
||||||
"sources_\\w+",
|
"sources_\\w+",
|
||||||
|
AppSettings.KEY_APP_PASSWORD,
|
||||||
|
AppSettings.KEY_PROXY_LOGIN,
|
||||||
|
AppSettings.KEY_PROXY_ADDRESS,
|
||||||
|
AppSettings.KEY_PROXY_PASSWORD,
|
||||||
)
|
)
|
||||||
httpSender {
|
httpSender {
|
||||||
uri = getString(R.string.url_error_report)
|
uri = getString(R.string.url_error_report)
|
||||||
@@ -83,8 +98,10 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
ReportField.PHONE_MODEL,
|
ReportField.PHONE_MODEL,
|
||||||
ReportField.STACK_TRACE,
|
ReportField.STACK_TRACE,
|
||||||
ReportField.CRASH_CONFIGURATION,
|
ReportField.CRASH_CONFIGURATION,
|
||||||
|
ReportField.CUSTOM_DATA,
|
||||||
ReportField.SHARED_PREFERENCES,
|
ReportField.SHARED_PREFERENCES,
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
text = getString(R.string.crash_text)
|
text = getString(R.string.crash_text)
|
||||||
title = getString(R.string.error_occurred)
|
title = getString(R.string.error_occurred)
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.data
|
package org.koitharu.kotatsu.bookmarks.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ abstract class BookmarksDao {
|
|||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
|
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at",
|
||||||
)
|
)
|
||||||
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||||
|
|
||||||
@@ -29,5 +33,8 @@ abstract class BookmarksDao {
|
|||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
abstract suspend fun delete(entity: BookmarkEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||||
abstract suspend fun delete(mangaId: Long, pageId: Long)
|
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
||||||
}
|
|
||||||
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
||||||
|
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.domain
|
package org.koitharu.kotatsu.bookmarks.domain
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.*
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class Bookmark(
|
class Bookmark(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
@@ -14,6 +15,20 @@ class Bookmark(
|
|||||||
val percent: Float,
|
val percent: Float,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val directImageUrl: String?
|
||||||
|
get() = if (isImageUrlDirect()) imageUrl else null
|
||||||
|
|
||||||
|
fun toMangaPage() = MangaPage(
|
||||||
|
id = pageId,
|
||||||
|
url = imageUrl,
|
||||||
|
preview = null,
|
||||||
|
source = manga.source,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun isImageUrlDirect(): Boolean {
|
||||||
|
return imageUrl.substringAfterLast('.').length in 2..4
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -27,9 +42,7 @@ class Bookmark(
|
|||||||
if (scroll != other.scroll) return false
|
if (scroll != other.scroll) return false
|
||||||
if (imageUrl != other.imageUrl) return false
|
if (imageUrl != other.imageUrl) return false
|
||||||
if (createdAt != other.createdAt) return false
|
if (createdAt != other.createdAt) return false
|
||||||
if (percent != other.percent) return false
|
return percent == other.percent
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -43,4 +56,4 @@ class Bookmark(
|
|||||||
result = 31 * result + percent.hashCode()
|
result = 31 * result + percent.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ import androidx.room.withTransaction
|
|||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
|
||||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||||
import org.koitharu.kotatsu.bookmarks.data.toBookmark
|
import org.koitharu.kotatsu.bookmarks.data.toBookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.data.toBookmarks
|
import org.koitharu.kotatsu.bookmarks.data.toBookmarks
|
||||||
@@ -14,9 +13,10 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
|||||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
@@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
|
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
|
||||||
db.bookmarksDao.delete(mangaId, pageId)
|
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
|
||||||
|
"Bookmark not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeBookmark(bookmark: Bookmark) {
|
||||||
|
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
||||||
@@ -3,16 +3,14 @@ package org.koitharu.kotatsu.bookmarks.ui
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||||
@@ -24,10 +22,10 @@ class BookmarksActivity :
|
|||||||
SnackbarOwner {
|
SnackbarOwner {
|
||||||
|
|
||||||
override val appBar: AppBarLayout
|
override val appBar: AppBarLayout
|
||||||
get() = binding.appbar
|
get() = viewBinding.appbar
|
||||||
|
|
||||||
override val snackbarHost: CoordinatorLayout
|
override val snackbarHost: CoordinatorLayout
|
||||||
get() = binding.root
|
get() = viewBinding.root
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -43,7 +41,7 @@ class BookmarksActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.root.updatePadding(
|
viewBinding.root.updatePadding(
|
||||||
left = insets.left,
|
left = insets.left,
|
||||||
right = insets.right,
|
right = insets.right,
|
||||||
)
|
)
|
||||||
@@ -16,19 +16,23 @@ import coil.ImageLoader
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
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.base.ui.list.fastscroll.FastScroller
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
|
||||||
import org.koitharu.kotatsu.bookmarks.data.ids
|
import org.koitharu.kotatsu.bookmarks.data.ids
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
|
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.reverseAsync
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
@@ -37,8 +41,6 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
|||||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
|
||||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -56,12 +58,12 @@ class BookmarksFragment :
|
|||||||
private var adapter: BookmarksGroupAdapter? = null
|
private var adapter: BookmarksGroupAdapter? = null
|
||||||
private var selectionController: SectionedSelectionController<Manga>? = null
|
private var selectionController: SectionedSelectionController<Manga>? = null
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
|
||||||
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
selectionController = SectionedSelectionController(
|
selectionController = SectionedSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
owner = this,
|
owner = this,
|
||||||
@@ -77,12 +79,12 @@ class BookmarksFragment :
|
|||||||
)
|
)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -93,8 +95,11 @@ class BookmarksFragment :
|
|||||||
|
|
||||||
override fun onItemClick(item: Bookmark, view: View) {
|
override fun onItemClick(item: Bookmark, view: View) {
|
||||||
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
||||||
val intent = ReaderActivity.newIntent(view.context, item)
|
val intent = ReaderActivity.IntentBuilder(view.context)
|
||||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
.bookmark(item)
|
||||||
|
.incognito(true)
|
||||||
|
.build()
|
||||||
|
startActivity(intent, scaleUpActivityOptionsOf(view))
|
||||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +119,7 @@ class BookmarksFragment :
|
|||||||
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
|
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
|
||||||
|
|
||||||
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
|
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
|
||||||
binding.recyclerView.invalidateNestedItemDecorations()
|
requireViewBinding().recyclerView.invalidateNestedItemDecorations()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(
|
override fun onCreateActionMode(
|
||||||
@@ -149,10 +154,10 @@ class BookmarksFragment :
|
|||||||
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
|
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.recyclerView.updatePadding(
|
requireViewBinding().recyclerView.updatePadding(
|
||||||
bottom = insets.bottom,
|
bottom = insets.bottom,
|
||||||
)
|
)
|
||||||
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = insets.bottom
|
bottomMargin = insets.bottom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,8 @@ import android.content.Context
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getItem
|
||||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
import org.koitharu.kotatsu.utils.ext.getItem
|
|
||||||
|
|
||||||
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
|
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
|
||||||
|
|
||||||
@@ -14,5 +14,4 @@ class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(
|
|||||||
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
|
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
|
||||||
return item.pageId
|
return item.pageId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui
|
package org.koitharu.kotatsu.bookmarks.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -25,9 +28,9 @@ class BookmarksViewModel @Inject constructor(
|
|||||||
private val repository: BookmarksRepository,
|
private val repository: BookmarksRepository,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
|
||||||
val content: LiveData<List<ListModel>> = repository.observeBookmarks()
|
val content: StateFlow<List<ListModel>> = repository.observeBookmarks()
|
||||||
.map { list ->
|
.map { list ->
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -43,12 +46,12 @@ class BookmarksViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
|
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val handle = repository.removeBookmarks(ids)
|
val handle = repository.removeBookmarks(ids)
|
||||||
onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle))
|
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,16 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
|
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
|
||||||
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.source
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
|
||||||
|
|
||||||
fun bookmarkListAD(
|
fun bookmarkListAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -28,7 +28,8 @@ fun bookmarkListAD(
|
|||||||
binding.root.setOnLongClickListener(listener)
|
binding.root.setOnLongClickListener(listener)
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
|
val data: Any = item.directImageUrl ?: item.toMangaPage()
|
||||||
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
|
||||||
size(CoverSizeResolver(binding.imageViewThumb))
|
size(CoverSizeResolver(binding.imageViewThumb))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
@@ -4,8 +4,8 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
|
||||||
class BookmarksAdapter(
|
class BookmarksAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -13,13 +13,15 @@ class BookmarksAdapter(
|
|||||||
clickListener: OnListItemClickListener<Bookmark>,
|
clickListener: OnListItemClickListener<Bookmark>,
|
||||||
) : AsyncListDifferDelegationAdapter<Bookmark>(
|
) : AsyncListDifferDelegationAdapter<Bookmark>(
|
||||||
DiffCallback(),
|
DiffCallback(),
|
||||||
bookmarkListAD(coil, lifecycleOwner, clickListener)
|
bookmarkListAD(coil, lifecycleOwner, clickListener),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||||
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId
|
return oldItem.manga.id == newItem.manga.id &&
|
||||||
|
oldItem.chapterId == newItem.chapterId &&
|
||||||
|
oldItem.page == newItem.page
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||||
@@ -27,4 +29,4 @@ class BookmarksAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,20 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
|
||||||
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.source
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
|
||||||
|
|
||||||
fun bookmarksGroupAD(
|
fun bookmarksGroupAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -5,16 +5,17 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ class BookmarksGroupAdapter(
|
|||||||
oldItem.manga.id == newItem.manga.id
|
oldItem.manga.id == newItem.manga.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
||||||
|
oldItem.key == newItem.key
|
||||||
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,10 +12,10 @@ import androidx.core.graphics.Insets
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
|
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -32,13 +32,13 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||||
}
|
}
|
||||||
with(binding.webView.settings) {
|
with(viewBinding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
userAgentString = CommonHeadersInterceptor.userAgentChrome
|
||||||
}
|
}
|
||||||
binding.webView.webViewClient = BrowserClient(this)
|
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||||
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
|
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(binding.webView)
|
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
return
|
return
|
||||||
@@ -51,18 +51,18 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
||||||
url,
|
url,
|
||||||
)
|
)
|
||||||
binding.webView.loadUrl(url)
|
viewBinding.webView.loadUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
binding.webView.saveState(outState)
|
viewBinding.webView.saveState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
binding.webView.restoreState(savedInstanceState)
|
viewBinding.webView.restoreState(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -73,14 +73,14 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
binding.webView.stopLoading()
|
viewBinding.webView.stopLoading()
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_browser -> {
|
R.id.action_browser -> {
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
intent.data = Uri.parse(binding.webView.url)
|
intent.data = Uri.parse(viewBinding.webView.url)
|
||||||
try {
|
try {
|
||||||
startActivity(Intent.createChooser(intent, item.title))
|
startActivity(Intent.createChooser(intent, item.title))
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
@@ -92,22 +92,23 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
binding.webView.onPause()
|
viewBinding.webView.onPause()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.webView.onResume()
|
viewBinding.webView.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding.webView.destroy()
|
viewBinding.webView.stopLoading()
|
||||||
|
viewBinding.webView.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
binding.progressBar.isVisible = isLoading
|
viewBinding.progressBar.isVisible = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||||
@@ -120,10 +121,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.appbar.updatePadding(
|
viewBinding.appbar.updatePadding(
|
||||||
top = insets.top,
|
top = insets.top,
|
||||||
)
|
)
|
||||||
binding.root.updatePadding(
|
viewBinding.root.updatePadding(
|
||||||
left = insets.left,
|
left = insets.left,
|
||||||
right = insets.right,
|
right = insets.right,
|
||||||
bottom = insets.bottom,
|
bottom = insets.bottom,
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||||
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
import javax.inject.Inject
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCallback {
|
||||||
|
|
||||||
|
private var pendingResult = RESULT_CANCELED
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cookieJar: MutableCookieJar
|
||||||
|
|
||||||
|
private var onBackPressedCallback: WebViewBackPressedCallback? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
supportActionBar?.run {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||||
|
}
|
||||||
|
val url = intent?.dataString.orEmpty()
|
||||||
|
with(viewBinding.webView.settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
|
||||||
|
}
|
||||||
|
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
||||||
|
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
|
||||||
|
onBackPressedDispatcher.addCallback(it)
|
||||||
|
}
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
finishAfterTransition()
|
||||||
|
} else {
|
||||||
|
onTitleChanged(getString(R.string.loading_), url)
|
||||||
|
viewBinding.webView.loadUrl(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
viewBinding.webView.run {
|
||||||
|
stopLoading()
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
viewBinding.webView.saveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
viewBinding.webView.restoreState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
viewBinding.appbar.updatePadding(
|
||||||
|
top = insets.top,
|
||||||
|
)
|
||||||
|
viewBinding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
viewBinding.webView.stopLoading()
|
||||||
|
finishAfterTransition()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewBinding.webView.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
viewBinding.webView.onPause()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
setResult(pendingResult)
|
||||||
|
super.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageLoaded() {
|
||||||
|
viewBinding.progressBar.isInvisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckPassed() {
|
||||||
|
pendingResult = RESULT_OK
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
viewBinding.progressBar.isVisible = isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHistoryChanged() {
|
||||||
|
onBackPressedCallback?.onHistoryChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||||
|
setTitle(title)
|
||||||
|
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
||||||
|
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent {
|
||||||
|
return newIntent(context, input.first, input.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
|
||||||
|
return TaggedActivityResult(TAG, resultCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG = "CloudFlareActivity"
|
||||||
|
private const val ARG_UA = "ua"
|
||||||
|
|
||||||
|
fun newIntent(
|
||||||
|
context: Context,
|
||||||
|
url: String,
|
||||||
|
headers: Headers?,
|
||||||
|
) = Intent(context, CloudFlareActivity::class.java).apply {
|
||||||
|
data = url.toUri()
|
||||||
|
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||||
|
putExtra(ARG_UA, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.AndroidRuntimeException
|
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import coil.ComponentRegistry
|
import coil.ComponentRegistry
|
||||||
@@ -23,51 +22,42 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import okhttp3.Cache
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.*
|
import org.koitharu.kotatsu.core.network.*
|
||||||
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
|
||||||
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
|
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||||
|
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.activityManager
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
|
|
||||||
import org.koitharu.kotatsu.utils.ext.activityManager
|
|
||||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface AppModule {
|
interface AppModule {
|
||||||
|
|
||||||
@Binds
|
|
||||||
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
||||||
|
|
||||||
@@ -76,53 +66,6 @@ interface AppModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideCookieJar(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
): MutableCookieJar = try {
|
|
||||||
AndroidCookieJar()
|
|
||||||
} catch (e: AndroidRuntimeException) {
|
|
||||||
// WebView is not available
|
|
||||||
PreferencesCookieJar(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideHttpCache(
|
|
||||||
localStorageManager: LocalStorageManager,
|
|
||||||
): Cache = localStorageManager.createHttpCache()
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideOkHttpClient(
|
|
||||||
cache: Cache,
|
|
||||||
commonHeadersInterceptor: CommonHeadersInterceptor,
|
|
||||||
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
|
||||||
cookieJar: CookieJar,
|
|
||||||
settings: AppSettings,
|
|
||||||
): OkHttpClient {
|
|
||||||
return OkHttpClient.Builder().apply {
|
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
|
||||||
cookieJar(cookieJar)
|
|
||||||
dns(DoHManager(cache, settings))
|
|
||||||
if (settings.isSSLBypassEnabled) {
|
|
||||||
bypassSSLErrors()
|
|
||||||
}
|
|
||||||
cache(cache)
|
|
||||||
addNetworkInterceptor(CacheLimitInterceptor())
|
|
||||||
addInterceptor(GZipInterceptor())
|
|
||||||
addInterceptor(commonHeadersInterceptor)
|
|
||||||
addInterceptor(CloudFlareInterceptor())
|
|
||||||
addInterceptor(mirrorSwitchInterceptor)
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
addInterceptor(CurlLoggingInterceptor())
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideNetworkState(
|
fun provideNetworkState(
|
||||||
@@ -141,14 +84,11 @@ interface AppModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideCoil(
|
fun provideCoil(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
okHttpClient: OkHttpClient,
|
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val httpClientFactory = {
|
|
||||||
okHttpClient.newBuilder()
|
|
||||||
.cache(null)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
val diskCacheFactory = {
|
val diskCacheFactory = {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
@@ -156,19 +96,21 @@ interface AppModule {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.okHttpClient(httpClientFactory)
|
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
|
||||||
.interceptorDispatcher(Dispatchers.Default)
|
.interceptorDispatcher(Dispatchers.Default)
|
||||||
.fetcherDispatcher(Dispatchers.IO)
|
.fetcherDispatcher(Dispatchers.IO)
|
||||||
.decoderDispatcher(Dispatchers.Default)
|
.decoderDispatcher(Dispatchers.Default)
|
||||||
.transformationDispatcher(Dispatchers.Default)
|
.transformationDispatcher(Dispatchers.Default)
|
||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
.allowRgb565(isLowRamDevice(context))
|
.allowRgb565(context.isLowRamDevice())
|
||||||
.components(
|
.components(
|
||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
|
.add(pageFetcherFactory)
|
||||||
|
.add(imageProxyInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
@@ -184,12 +126,12 @@ interface AppModule {
|
|||||||
@ElementsIntoSet
|
@ElementsIntoSet
|
||||||
fun provideDatabaseObservers(
|
fun provideDatabaseObservers(
|
||||||
widgetUpdater: WidgetUpdater,
|
widgetUpdater: WidgetUpdater,
|
||||||
shortcutsUpdater: ShortcutsUpdater,
|
appShortcutManager: AppShortcutManager,
|
||||||
backupObserver: BackupObserver,
|
backupObserver: BackupObserver,
|
||||||
syncController: SyncController,
|
syncController: SyncController,
|
||||||
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
|
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
|
||||||
widgetUpdater,
|
widgetUpdater,
|
||||||
shortcutsUpdater,
|
appShortcutManager,
|
||||||
backupObserver,
|
backupObserver,
|
||||||
syncController,
|
syncController,
|
||||||
)
|
)
|
||||||
@@ -200,10 +142,12 @@ interface AppModule {
|
|||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
incognitoModeIndicator: IncognitoModeIndicator,
|
incognitoModeIndicator: IncognitoModeIndicator,
|
||||||
|
acraScreenLogger: AcraScreenLogger,
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
incognitoModeIndicator,
|
incognitoModeIndicator,
|
||||||
|
acraScreenLogger,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.BuildConfig
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 10
|
private const val PAGE_SIZE = 10
|
||||||
@@ -5,10 +5,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okio.Closeable
|
import okio.Closeable
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.format
|
||||||
import org.koitharu.kotatsu.core.zip.ZipOutput
|
import org.koitharu.kotatsu.core.zip.ZipOutput
|
||||||
import org.koitharu.kotatsu.utils.ext.format
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
|
|
||||||
class BackupZipOutput(val file: File) : Closeable {
|
class BackupZipOutput(val file: File) : Closeable {
|
||||||
@@ -42,4 +43,4 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl
|
|||||||
append(".bk.zip")
|
append(".bk.zip")
|
||||||
}
|
}
|
||||||
BackupZipOutput(File(dir, filename))
|
BackupZipOutput(File(dir, filename))
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration11To12
|
|||||||
import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
||||||
|
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||||
@@ -33,6 +34,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration6To7
|
|||||||
import org.koitharu.kotatsu.core.db.migrations.Migration7To8
|
import org.koitharu.kotatsu.core.db.migrations.Migration7To8
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration8To9
|
import org.koitharu.kotatsu.core.db.migrations.Migration8To9
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration9To10
|
import org.koitharu.kotatsu.core.db.migrations.Migration9To10
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
@@ -46,9 +48,8 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
|||||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
|
||||||
|
|
||||||
const val DATABASE_VERSION = 15
|
const val DATABASE_VERSION = 16
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -100,6 +101,7 @@ val databaseMigrations: Array<Migration>
|
|||||||
Migration12To13(),
|
Migration12To13(),
|
||||||
Migration13To14(),
|
Migration13To14(),
|
||||||
Migration14To15(),
|
Migration14To15(),
|
||||||
|
Migration15To16(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
|
||||||
|
|
||||||
// Entity to model
|
// Entity to model
|
||||||
|
|
||||||
@@ -23,4 +23,5 @@ data class MangaPrefsEntity(
|
|||||||
@ColumnInfo(name = "mode") val mode: Int,
|
@ColumnInfo(name = "mode") val mode: Int,
|
||||||
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
||||||
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
|
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
|
||||||
|
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
|
||||||
)
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user