Compare commits
92 Commits
v4.0-beta1
...
v4.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff58539e2e | ||
| d8e7689a94 | |||
| 32cfbb327c | |||
| 245e87256b | |||
| ed8c69037f | |||
|
|
3f76d22d67 | ||
| 980988e684 | |||
|
|
347811abb6 | ||
|
|
ccb8b0c8e7 | ||
|
|
18137ab48e | ||
|
|
a9f435ae3d | ||
| 0758cfef64 | |||
| 06d1d56448 | |||
| 07c70eaccc | |||
| 5ad6413952 | |||
|
|
0b9d9ac7f2 | ||
| 3ab87027ab | |||
|
|
7545a774ba | ||
| db16eb8e29 | |||
|
|
e5a27a7c6f | ||
|
|
d26bc102d1 | ||
|
|
fc6a8afd93 | ||
| 5a9d401446 | |||
| 77ac40b445 | |||
| a29454f672 | |||
| 80ee7c8e54 | |||
| fb202f80a5 | |||
| 45dbd5aa44 | |||
| ee65251bf5 | |||
| eaeb11f9ce | |||
| 2f74633abb | |||
| 0f346dc725 | |||
| 1b92848964 | |||
| 3d91583585 | |||
| f76d9fa3e4 | |||
| b00b2e406e | |||
| 74717e2b93 | |||
| 9b54ed6bc7 | |||
| 7b36c64b34 | |||
| da09884136 | |||
| 64aaf37556 | |||
| 11104223eb | |||
| 0c119bc137 | |||
| 5c058e626b | |||
| 2005ae2bf3 | |||
| d0650c7cf4 | |||
|
|
2df4e6480a | ||
|
|
017a1686dc | ||
|
|
279dc03695 | ||
|
|
c8c482f692 | ||
|
|
02a0e3ebcd | ||
|
|
fc0b3f3b38 | ||
|
|
2925900214 | ||
|
|
eae370e41c | ||
|
|
d0338a604a | ||
|
|
e22b98b476 | ||
| 4d838d290d | |||
| 048efdf59f | |||
| 65dbc6b8e5 | |||
| 627a00beb4 | |||
| e00ed13ad1 | |||
| af2adeba13 | |||
| 893fa6bd90 | |||
| 512188c8dd | |||
| aae6761809 | |||
| c3f055d0c4 | |||
| 93c6bec452 | |||
|
|
04d5df20d1 | ||
|
|
665eca0699 | ||
|
|
9a1534464f | ||
|
|
f856fc6fac | ||
| 4af8e73303 | |||
| 23239f1fec | |||
| 853e4d6fde | |||
| 14a37ad16e | |||
| c944044465 | |||
| 8a63ca2310 | |||
| 12e5e3b35e | |||
| 553a85ef86 | |||
| d604ff3c24 | |||
|
|
4f9eee7d46 | ||
|
|
fe673a94ed | ||
| bcd891d653 | |||
| 1e75edf262 | |||
| 73478d6a81 | |||
| 982080a930 | |||
| 66cd5070dc | |||
|
|
c05b0eaa59 | ||
|
|
29a073d844 | ||
| a0e69428e4 | |||
| de7012cabf | |||
| 3f6a103915 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
|||||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
custom: ["https://yoomoney.ru/to/410012543938752"]
|
||||||
|
ko_fi: koitharu
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
/.idea/misc.xml
|
||||||
/.idea/discord.xml
|
/.idea/discord.xml
|
||||||
|
/.idea/compiler.xml
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
|
|||||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="11" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
3
.idea/kotlinc.xml
generated
3
.idea/kotlinc.xml
generated
@@ -3,4 +3,7 @@
|
|||||||
<component name="Kotlin2JvmCompilerArguments">
|
<component name="Kotlin2JvmCompilerArguments">
|
||||||
<option name="jvmTarget" value="1.8" />
|
<option name="jvmTarget" value="1.8" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.7.20" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 497
|
versionCode 501
|
||||||
versionName '4.0-beta1'
|
versionName '4.0.1'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ android {
|
|||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||||
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
||||||
|
'-opt-in=com.google.android.material.badge.ExperimentalBadgeUtils',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
@@ -82,15 +83,15 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:b3a9c5fcda') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:a1441e7ed7') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
implementation 'androidx.activity:activity-ktx:1.6.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
||||||
@@ -101,8 +102,8 @@ dependencies {
|
|||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.7.0-rc01'
|
implementation 'com.google.android.material:material:1.7.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
|
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
|
||||||
|
|
||||||
@@ -117,14 +118,14 @@ dependencies {
|
|||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||||
|
|
||||||
implementation "com.google.dagger:hilt-android:2.43.2"
|
implementation "com.google.dagger:hilt-android:2.44"
|
||||||
kapt "com.google.dagger:hilt-compiler:2.43.2"
|
kapt "com.google.dagger:hilt-compiler:2.44"
|
||||||
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.2.1'
|
implementation 'io.coil-kt:coil-base:2.2.2'
|
||||||
implementation 'io.coil-kt:coil-svg:2.2.1'
|
implementation 'io.coil-kt:coil-svg:2.2.2'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:a9ab74c8b4'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:f8a38b08fe'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
|
|
||||||
implementation 'ch.acra:acra-http:5.9.6'
|
implementation 'ch.acra:acra-http:5.9.6'
|
||||||
@@ -133,7 +134,7 @@ dependencies {
|
|||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20220320'
|
testImplementation 'org.json:json:20220924'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||||
@@ -146,6 +147,6 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
||||||
|
|
||||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
|
||||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
||||||
android:label="@string/history" />
|
android:label="@string/history" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity"
|
||||||
|
android:label="@string/updates" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||||
android:label="@string/favourites" />
|
android:label="@string/favourites" />
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.hilt.work.HiltWorkerFactory
|
|||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.acra.ReportField
|
import org.acra.ReportField
|
||||||
@@ -25,6 +24,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
|||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class KotatsuApp : Application(), Configuration.Provider {
|
class KotatsuApp : Application(), Configuration.Provider {
|
||||||
@@ -72,6 +72,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
reportContent = listOf(
|
reportContent = listOf(
|
||||||
ReportField.PACKAGE_NAME,
|
ReportField.PACKAGE_NAME,
|
||||||
|
ReportField.INSTALLATION_ID,
|
||||||
ReportField.APP_VERSION_CODE,
|
ReportField.APP_VERSION_CODE,
|
||||||
ReportField.APP_VERSION_NAME,
|
ReportField.APP_VERSION_NAME,
|
||||||
ReportField.ANDROID_VERSION,
|
ReportField.ANDROID_VERSION,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
fun interface ReversibleHandle {
|
fun interface ReversibleHandle {
|
||||||
|
|
||||||
@@ -11,8 +14,10 @@ fun interface ReversibleHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
|
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
reverse()
|
withContext(NonCancellable) {
|
||||||
|
reverse()
|
||||||
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
}
|
}
|
||||||
@@ -21,4 +26,4 @@ fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.D
|
|||||||
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
|
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
|
||||||
this.reverse()
|
this.reverse()
|
||||||
other.reverse()
|
other.reverse()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
||||||
@@ -30,6 +29,7 @@ import org.koitharu.kotatsu.base.ui.util.inject
|
|||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
abstract class BaseActivity<B : ViewBinding> :
|
abstract class BaseActivity<B : ViewBinding> :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
@@ -134,6 +134,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
|
window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if ( // https://issuetracker.google.com/issues/139738913
|
if ( // https://issuetracker.google.com/issues/139738913
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
abstract class CoroutineIntentService : BaseService() {
|
abstract class CoroutineIntentService : BaseService() {
|
||||||
|
|
||||||
@@ -21,11 +23,13 @@ abstract class CoroutineIntentService : BaseService() {
|
|||||||
return Service.START_REDELIVER_INTENT
|
return Service.START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
|
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch(errorHandler(startId)) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
try {
|
try {
|
||||||
withContext(dispatcher) {
|
if (intent != null) {
|
||||||
processIntent(intent)
|
withContext(dispatcher) {
|
||||||
|
processIntent(startId, intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
@@ -33,5 +37,12 @@ abstract class CoroutineIntentService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract suspend fun processIntent(intent: Intent?)
|
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
||||||
|
|
||||||
|
protected abstract fun onError(startId: Int, error: Throwable)
|
||||||
|
|
||||||
|
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
throwable.printStackTraceDebug()
|
||||||
|
onError(startId, throwable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
interface DefaultActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||||
|
|
||||||
|
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
package org.koitharu.kotatsu.base.ui.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application.ActivityLifecycleCallbacks
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import java.util.*
|
import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
|
||||||
|
import java.util.WeakHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallbacks {
|
class ActivityRecreationHandle @Inject constructor() : DefaultActivityLifecycleCallbacks {
|
||||||
|
|
||||||
private val activities = WeakHashMap<Activity, Unit>()
|
private val activities = WeakHashMap<Activity, Unit>()
|
||||||
|
|
||||||
@@ -16,16 +16,6 @@ class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallback
|
|||||||
activities[activity] = Unit
|
activities[activity] = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityStarted(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityStopped(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
||||||
|
|
||||||
override fun onActivityDestroyed(activity: Activity) {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
activities.remove(activity)
|
activities.remove(activity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import dagger.hilt.InstallIn
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.ElementsIntoSet
|
import dagger.multibindings.ElementsIntoSet
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -40,9 +38,12 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|||||||
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.isLowRamDevice
|
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
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
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@@ -152,9 +153,11 @@ interface AppModule {
|
|||||||
fun provideActivityLifecycleCallbacks(
|
fun provideActivityLifecycleCallbacks(
|
||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
|
incognitoModeIndicator: IncognitoModeIndicator,
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
|
incognitoModeIndicator,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import javax.inject.Inject
|
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
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 javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 10
|
private const val PAGE_SIZE = 10
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
JsonDeserializer(it).toTagEntity()
|
JsonDeserializer(it).toTagEntity()
|
||||||
}
|
}
|
||||||
val history = JsonDeserializer(item).toHistoryEntity()
|
val history = JsonDeserializer(item).toHistoryEntity()
|
||||||
result += runCatching {
|
result += runCatchingCancellable {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
db.tagsDao.upsert(tags)
|
||||||
db.mangaDao.upsert(manga, tags)
|
db.mangaDao.upsert(manga, tags)
|
||||||
@@ -100,7 +101,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
val result = CompositeResult()
|
val result = CompositeResult()
|
||||||
for (item in entry.data.JSONIterator()) {
|
for (item in entry.data.JSONIterator()) {
|
||||||
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
|
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
|
||||||
result += runCatching {
|
result += runCatchingCancellable {
|
||||||
db.favouriteCategoriesDao.upsert(category)
|
db.favouriteCategoriesDao.upsert(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
JsonDeserializer(it).toTagEntity()
|
JsonDeserializer(it).toTagEntity()
|
||||||
}
|
}
|
||||||
val favourite = JsonDeserializer(item).toFavouriteEntity()
|
val favourite = JsonDeserializer(item).toFavouriteEntity()
|
||||||
result += runCatching {
|
result += runCatchingCancellable {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
db.tagsDao.upsert(tags)
|
||||||
db.mangaDao.upsert(manga, tags)
|
db.mangaDao.upsert(manga, tags)
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -24,6 +17,14 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
|
|||||||
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||||
import org.koitharu.kotatsu.utils.ext.asArrayList
|
import org.koitharu.kotatsu.utils.ext.asArrayList
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
|
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ class AppUpdateRepository @Inject constructor(
|
|||||||
if (!isUpdateSupported()) {
|
if (!isUpdateSupported()) {
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
|
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
|
||||||
val available = getAvailableVersions().asArrayList()
|
val available = getAvailableVersions().asArrayList()
|
||||||
available.sortBy { it.versionId }
|
available.sortBy { it.versionId }
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package org.koitharu.kotatsu.core.model.parcelable
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import androidx.core.os.ParcelCompat
|
import androidx.core.os.ParcelCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.utils.ext.readParcelableCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.readSerializableCompat
|
||||||
|
|
||||||
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
|
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
|
||||||
out.writeLong(id)
|
out.writeLong(id)
|
||||||
@@ -37,11 +42,11 @@ fun Parcel.readManga() = Manga(
|
|||||||
coverUrl = requireNotNull(readString()),
|
coverUrl = requireNotNull(readString()),
|
||||||
largeCoverUrl = readString(),
|
largeCoverUrl = readString(),
|
||||||
description = readString(),
|
description = readString(),
|
||||||
tags = requireNotNull(readParcelable<ParcelableMangaTags>(ParcelableMangaTags::class.java.classLoader)).tags,
|
tags = requireNotNull(readParcelableCompat<ParcelableMangaTags>()).tags,
|
||||||
state = readSerializable() as MangaState?,
|
state = readSerializableCompat(),
|
||||||
author = readString(),
|
author = readString(),
|
||||||
chapters = readParcelable<ParcelableMangaChapters>(ParcelableMangaChapters::class.java.classLoader)?.chapters,
|
chapters = readParcelableCompat<ParcelableMangaChapters>()?.chapters,
|
||||||
source = readSerializable() as MangaSource,
|
source = checkNotNull(readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaPage.writeToParcel(out: Parcel) {
|
fun MangaPage.writeToParcel(out: Parcel) {
|
||||||
@@ -57,7 +62,7 @@ fun Parcel.readMangaPage() = MangaPage(
|
|||||||
url = requireNotNull(readString()),
|
url = requireNotNull(readString()),
|
||||||
referer = requireNotNull(readString()),
|
referer = requireNotNull(readString()),
|
||||||
preview = readString(),
|
preview = readString(),
|
||||||
source = readSerializable() as MangaSource,
|
source = checkNotNull(readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaChapter.writeToParcel(out: Parcel) {
|
fun MangaChapter.writeToParcel(out: Parcel) {
|
||||||
@@ -79,7 +84,7 @@ fun Parcel.readMangaChapter() = MangaChapter(
|
|||||||
scanlator = readString(),
|
scanlator = readString(),
|
||||||
uploadDate = readLong(),
|
uploadDate = readLong(),
|
||||||
branch = readString(),
|
branch = readString(),
|
||||||
source = readSerializable() as MangaSource,
|
source = checkNotNull(readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaTag.writeToParcel(out: Parcel) {
|
fun MangaTag.writeToParcel(out: Parcel) {
|
||||||
@@ -91,5 +96,5 @@ fun MangaTag.writeToParcel(out: Parcel) {
|
|||||||
fun Parcel.readMangaTag() = MangaTag(
|
fun Parcel.readMangaTag() = MangaTag(
|
||||||
title = requireNotNull(readString()),
|
title = requireNotNull(readString()),
|
||||||
key = requireNotNull(readString()),
|
key = requireNotNull(readString()),
|
||||||
source = readSerializable() as MangaSource,
|
source = checkNotNull(readSerializableCompat()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,4 +50,4 @@ class ParcelableManga(
|
|||||||
return arrayOfNulls(size)
|
return arrayOfNulls(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.koitharu.kotatsu.core.os
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager.NetworkCallback
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.channels.ProducerScope
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class NetworkStateObserver @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
) : StateFlow<Boolean> {
|
||||||
|
|
||||||
|
private val connectivityManager = context.connectivityManager
|
||||||
|
|
||||||
|
override val replayCache: List<Boolean>
|
||||||
|
get() = listOf(value)
|
||||||
|
|
||||||
|
override var value: Boolean = connectivityManager.isNetworkAvailable
|
||||||
|
|
||||||
|
override suspend fun collect(collector: FlowCollector<Boolean>): Nothing {
|
||||||
|
collector.emit(value)
|
||||||
|
while (true) {
|
||||||
|
observeImpl().collect(collector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeImpl() = callbackFlow<Boolean> {
|
||||||
|
val request = NetworkRequest.Builder().build()
|
||||||
|
val callback = FlowNetworkCallback(this)
|
||||||
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
|
awaitClose {
|
||||||
|
connectivityManager.unregisterNetworkCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class FlowNetworkCallback(
|
||||||
|
private val producerScope: ProducerScope<Boolean>,
|
||||||
|
) : NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) = update()
|
||||||
|
|
||||||
|
override fun onLost(network: Network) = update()
|
||||||
|
|
||||||
|
override fun onUnavailable() = update()
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
val newValue = connectivityManager.isNetworkAvailable
|
||||||
|
if (value != newValue) {
|
||||||
|
value = newValue
|
||||||
|
producerScope.trySendBlocking(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@ import coil.request.ImageRequest
|
|||||||
import coil.size.Precision
|
import coil.size.Precision
|
||||||
import coil.size.Scale
|
import coil.size.Scale
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -32,6 +30,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ShortcutsUpdater @Inject constructor(
|
class ShortcutsUpdater @Inject constructor(
|
||||||
@@ -92,7 +93,7 @@ class ShortcutsUpdater @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||||
private suspend fun updateShortcutsImpl() = runCatching {
|
private suspend fun updateShortcutsImpl() = runCatchingCancellable {
|
||||||
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
|
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
|
||||||
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
|
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
|
||||||
.filter { x -> x.title.isNotEmpty() }
|
.filter { x -> x.title.isNotEmpty() }
|
||||||
@@ -112,7 +113,7 @@ class ShortcutsUpdater @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
|
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
|
||||||
val icon = runCatching {
|
val icon = runCatchingCancellable {
|
||||||
coil.execute(
|
coil.execute(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(manga.coverUrl)
|
.data(manga.coverUrl)
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||||
@@ -26,6 +20,14 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue
|
|||||||
import org.koitharu.kotatsu.utils.ext.observe
|
import org.koitharu.kotatsu.utils.ext.observe
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||||
@@ -206,6 +208,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isReaderBarEnabled: Boolean
|
val isReaderBarEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
||||||
|
|
||||||
|
val isReaderSliderEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
@@ -213,6 +218,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
||||||
|
|
||||||
|
val isWebtoonZoomEnable: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||||
|
|
||||||
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
||||||
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
||||||
NETWORK_ALWAYS -> true
|
NETWORK_ALWAYS -> true
|
||||||
@@ -328,9 +336,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_INCOGNITO_MODE = "incognito"
|
const val KEY_INCOGNITO_MODE = "incognito"
|
||||||
const val KEY_SYNC = "sync"
|
const val KEY_SYNC = "sync"
|
||||||
const val KEY_READER_BAR = "reader_bar"
|
const val KEY_READER_BAR = "reader_bar"
|
||||||
|
const val KEY_READER_SLIDER = "reader_slider"
|
||||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||||
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
|
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
|
||||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||||
|
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||||
|
|
||||||
// About
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
|
|||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.report) { _, _ ->
|
.setPositiveButton(R.string.report) { _, _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
error.report(TAG)
|
error.report()
|
||||||
}.setTitle(R.string.error_occurred)
|
}.setTitle(R.string.error_occurred)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ import androidx.core.view.isGone
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||||
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 javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||||
@@ -37,12 +35,17 @@ import org.koitharu.kotatsu.core.ui.MangaErrorDialog
|
|||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.bindBadge
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||||
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.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ViewBadge
|
||||||
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isReportable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DetailsActivity :
|
class DetailsActivity :
|
||||||
@@ -60,7 +63,7 @@ class DetailsActivity :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||||
|
|
||||||
private var badge: BadgeDrawable? = null
|
private lateinit var viewBadge: ViewBadge
|
||||||
|
|
||||||
private val viewModel: DetailsViewModel by assistedViewModels {
|
private val viewModel: DetailsViewModel by assistedViewModels {
|
||||||
viewModelFactory.create(MangaIntent(intent))
|
viewModelFactory.create(MangaIntent(intent))
|
||||||
@@ -83,6 +86,7 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
binding.buttonRead.setOnClickListener(this)
|
binding.buttonRead.setOnClickListener(this)
|
||||||
binding.buttonDropdown.setOnClickListener(this)
|
binding.buttonDropdown.setOnClickListener(this)
|
||||||
|
viewBadge = ViewBadge(binding.buttonRead, this)
|
||||||
|
|
||||||
chaptersMenuProvider = if (binding.layoutBottom != null) {
|
chaptersMenuProvider = if (binding.layoutBottom != null) {
|
||||||
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
|
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
|
||||||
@@ -151,6 +155,7 @@ class DetailsActivity :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_dropdown -> showBranchPopupMenu()
|
R.id.button_dropdown -> showBranchPopupMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,10 +193,12 @@ class DetailsActivity :
|
|||||||
ExceptionResolver.canResolve(e) -> {
|
ExceptionResolver.canResolve(e) -> {
|
||||||
resolveError(e)
|
resolveError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
manga == null -> {
|
manga == null -> {
|
||||||
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val snackbar = makeSnackbar(
|
val snackbar = makeSnackbar(
|
||||||
e.getDisplayMessage(resources),
|
e.getDisplayMessage(resources),
|
||||||
@@ -242,7 +249,7 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onNewChaptersChanged(newChapters: Int) {
|
private fun onNewChaptersChanged(newChapters: Int) {
|
||||||
badge = binding.buttonRead.bindBadge(badge, newChapters)
|
viewBadge.counter = newChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChapterMissingDialog(chapterId: Long) {
|
fun showChapterMissingDialog(chapterId: Long) {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class DetailsViewModel @AssistedInject constructor(
|
class DetailsViewModel @AssistedInject constructor(
|
||||||
@Assisted intent: MangaIntent,
|
@Assisted intent: MangaIntent,
|
||||||
@@ -189,7 +190,7 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
||||||
val original = localMangaRepository.getRemoteManga(manga)
|
val original = localMangaRepository.getRemoteManga(manga)
|
||||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
historyRepository.deleteOrSwap(manga, original)
|
||||||
}
|
}
|
||||||
onMangaRemoved.postCall(manga)
|
onMangaRemoved.postCall(manga)
|
||||||
@@ -228,7 +229,7 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
reload()
|
reload()
|
||||||
} else {
|
} else {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
localMangaRepository.getDetails(downloadedManga)
|
localMangaRepository.getDetails(downloadedManga)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
delegate.relatedManga.value = it
|
delegate.relatedManga.value = it
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class MangaDetailsDelegate(
|
class MangaDetailsDelegate(
|
||||||
private val intent: MangaIntent,
|
private val intent: MangaIntent,
|
||||||
@@ -45,9 +46,9 @@ class MangaDetailsDelegate(
|
|||||||
val hist = historyRepository.getOne(manga)
|
val hist = historyRepository.getOne(manga)
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
selectedBranch.value = manga.getPreferredBranch(hist)
|
||||||
mangaData.value = manga
|
mangaData.value = manga
|
||||||
relatedManga.value = runCatching {
|
relatedManga.value = runCatchingCancellable {
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
if (manga.source == MangaSource.LOCAL) {
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
|
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
||||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
mangaRepositoryFactory.create(m.source).getDetails(m)
|
||||||
} else {
|
} else {
|
||||||
localMangaRepository.findSavedManga(manga)
|
localMangaRepository.findSavedManga(manga)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun chapterListItemAD(
|
|||||||
when (item.status) {
|
when (item.status) {
|
||||||
FLAG_UNREAD -> {
|
FLAG_UNREAD -> {
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
|
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
|
||||||
}
|
}
|
||||||
FLAG_CURRENT -> {
|
FLAG_CURRENT -> {
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
||||||
@@ -53,4 +53,4 @@ fun chapterListItemAD(
|
|||||||
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
|
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
|
||||||
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
|
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,18 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
@@ -32,7 +39,9 @@ import org.koitharu.kotatsu.parsers.util.await
|
|||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
private const val MAX_FAILSAFE_ATTEMPTS = 2
|
private const val MAX_FAILSAFE_ATTEMPTS = 2
|
||||||
private const val DOWNLOAD_ERROR_DELAY = 500L
|
private const val DOWNLOAD_ERROR_DELAY = 500L
|
||||||
@@ -231,7 +240,7 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadCover(manga: Manga) = runCatching {
|
private suspend fun loadCover(manga: Manga) = runCatchingCancellable {
|
||||||
imageLoader.execute(
|
imageLoader.execute(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(manga.coverUrl)
|
.data(manga.coverUrl)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
|||||||
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
val adapter = DownloadsAdapter(lifecycleScope, coil)
|
val adapter = DownloadsAdapter(lifecycleScope, coil)
|
||||||
val spacing = resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.collections.set
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -28,10 +30,14 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|||||||
import org.koitharu.kotatsu.download.domain.DownloadManager
|
import org.koitharu.kotatsu.download.domain.DownloadManager
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||||
import org.koitharu.kotatsu.utils.ext.throttle
|
import org.koitharu.kotatsu.utils.ext.throttle
|
||||||
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||||
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
|
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DownloadService : BaseService() {
|
class DownloadService : BaseService() {
|
||||||
@@ -65,7 +71,7 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga
|
val manga = intent?.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
|
||||||
val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)
|
val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)
|
||||||
return if (manga != null) {
|
return if (manga != null) {
|
||||||
jobs[startId] = downloadManga(startId, manga, chapters)
|
jobs[startId] = downloadManga(startId, manga, chapters)
|
||||||
@@ -84,7 +90,9 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(controlReceiver)
|
unregisterReceiver(controlReceiver)
|
||||||
wakeLock.release()
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
isRunning = false
|
isRunning = false
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -168,6 +176,7 @@ class DownloadService : BaseService() {
|
|||||||
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
||||||
jobs[cancelId]?.cancel()
|
jobs[cancelId]?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_DOWNLOAD_RESUME -> {
|
ACTION_DOWNLOAD_RESUME -> {
|
||||||
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
||||||
jobs[cancelId]?.resume()
|
jobs[cancelId]?.resume()
|
||||||
@@ -254,7 +263,7 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
fun getDownloadedManga(intent: Intent?): Manga? {
|
fun getDownloadedManga(intent: Intent?): Manga? {
|
||||||
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
|
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
|
||||||
return intent.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga
|
return intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
sources.mapTo(result) { ExploreItem.Source(it) }
|
sources.mapTo(result) { ExploreItem.Source(it) }
|
||||||
} else {
|
} else {
|
||||||
result += ExploreItem.EmptyHint(
|
result += ExploreItem.EmptyHint(
|
||||||
icon = R.drawable.ic_empty_search,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.no_manga_sources,
|
textPrimary = R.string.no_manga_sources,
|
||||||
textSecondary = R.string.no_manga_sources_text,
|
textSecondary = R.string.no_manga_sources_text,
|
||||||
actionStringRes = R.string.manage,
|
actionStringRes = R.string.manage,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.model
|
package org.koitharu.kotatsu.explore.ui.model
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
@@ -73,6 +72,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
class EmptyHint(
|
class EmptyHint(
|
||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
@StringRes textPrimary: Int,
|
@StringRes textPrimary: Int,
|
||||||
@@ -81,4 +81,4 @@ sealed interface ExploreItem : ListModel {
|
|||||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
|
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
|
||||||
|
|
||||||
object Loading : ExploreItem
|
object Loading : ExploreItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|||||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||||
|
import kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
||||||
@@ -28,6 +29,7 @@ class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
|
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class FavouritesListViewModel @AssistedInject constructor(
|
class FavouritesListViewModel @AssistedInject constructor(
|
||||||
@Assisted private val categoryId: Long,
|
@Assisted private val categoryId: Long,
|
||||||
@@ -69,6 +70,7 @@ class FavouritesListViewModel @AssistedInject constructor(
|
|||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> list.toUi(mode, this)
|
else -> list.toUi(mode, this)
|
||||||
}
|
}
|
||||||
}.catch {
|
}.catch {
|
||||||
@@ -79,7 +81,7 @@ class FavouritesListViewModel @AssistedInject constructor(
|
|||||||
if (categoryId != NO_ID) {
|
if (categoryId != NO_ID) {
|
||||||
launchJob {
|
launchJob {
|
||||||
categoryName = withContext(Dispatchers.Default) {
|
categoryName = withContext(Dispatchers.Default) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
repository.getCategory(categoryId).title
|
repository.getCategory(categoryId).title
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.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 kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HistoryActivity :
|
class HistoryActivity :
|
||||||
@@ -28,6 +29,7 @@ class HistoryActivity :
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = HistoryListFragment.newInstance()
|
val fragment = HistoryListFragment.newInstance()
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.library.domain
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
|
||||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
class LibraryRepository @Inject constructor(
|
|
||||||
private val db: MangaDatabase,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
|
||||||
return db.favouriteCategoriesDao.observeAll()
|
|
||||||
.flatMapLatest { categories ->
|
|
||||||
val cats = categories.filter { it.isVisibleInLibrary }
|
|
||||||
if (cats.isEmpty()) {
|
|
||||||
flowOf(emptyMap())
|
|
||||||
} else {
|
|
||||||
observeCategoriesContent(cats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeCategoriesContent(
|
|
||||||
categories: List<FavouriteCategoryEntity>,
|
|
||||||
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(
|
|
||||||
categories.map { cat ->
|
|
||||||
val category = cat.toFavouriteCategory()
|
|
||||||
db.favouritesDao.observeAll(category.id, category.order)
|
|
||||||
.map { category to it.map { x -> x.manga.toManga(x.tags.toMangaTags()) } }
|
|
||||||
},
|
|
||||||
) { array -> array.toMap() }
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
interface LibraryListEventListener : ListStateHolderListener {
|
|
||||||
|
|
||||||
fun onItemClick(item: Manga, section: LibrarySectionModel, view: View)
|
|
||||||
|
|
||||||
fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean
|
|
||||||
|
|
||||||
fun onSectionClick(section: LibrarySectionModel, view: View)
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.model
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
|
||||||
|
|
||||||
sealed class LibrarySectionModel(
|
|
||||||
val items: List<MangaItemModel>,
|
|
||||||
@StringRes val showAllButtonText: Int,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
abstract val key: Any
|
|
||||||
abstract fun getTitle(resources: Resources): CharSequence
|
|
||||||
|
|
||||||
class History(
|
|
||||||
items: List<MangaItemModel>,
|
|
||||||
val timeAgo: DateTimeAgo?,
|
|
||||||
showAllButtonText: Int,
|
|
||||||
) : LibrarySectionModel(items, showAllButtonText) {
|
|
||||||
|
|
||||||
override val key: Any
|
|
||||||
get() = timeAgo?.javaClass ?: this::class.java
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): CharSequence {
|
|
||||||
return timeAgo?.format(resources) ?: resources.getString(R.string.history)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as History
|
|
||||||
|
|
||||||
if (timeAgo != other.timeAgo) return false
|
|
||||||
if (showAllButtonText != other.showAllButtonText) return false
|
|
||||||
if (items != other.items) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = items.hashCode()
|
|
||||||
result = 31 * result + (timeAgo?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + showAllButtonText.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "hist_$timeAgo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Favourites(
|
|
||||||
items: List<MangaItemModel>,
|
|
||||||
val category: FavouriteCategory,
|
|
||||||
showAllButtonText: Int,
|
|
||||||
) : LibrarySectionModel(items, showAllButtonText) {
|
|
||||||
|
|
||||||
override val key: Any
|
|
||||||
get() = category.id
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): CharSequence {
|
|
||||||
return category.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Favourites
|
|
||||||
|
|
||||||
if (category != other.category) return false
|
|
||||||
if (showAllButtonText != other.showAllButtonText) return false
|
|
||||||
if (items != other.items) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = items.hashCode()
|
|
||||||
result = 31 * result + category.hashCode()
|
|
||||||
result = 31 * result + showAllButtonText.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "fav_${category.id}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,24 +6,23 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ListModeSelectDialog :
|
class ListModeBottomSheet :
|
||||||
AlertDialogFragment<DialogListModeBinding>(),
|
BaseBottomSheet<DialogListModeBinding>(),
|
||||||
CheckableButtonGroup.OnCheckedChangeListener,
|
Slider.OnChangeListener,
|
||||||
Slider.OnChangeListener {
|
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
@@ -33,13 +32,6 @@ class ListModeSelectDialog :
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
) = DialogListModeBinding.inflate(inflater, container, false)
|
) = DialogListModeBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
|
||||||
return super.onBuildDialog(builder)
|
|
||||||
.setTitle(R.string.list_mode)
|
|
||||||
.setPositiveButton(R.string.done, null)
|
|
||||||
.setCancelable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val mode = settings.listMode
|
val mode = settings.listMode
|
||||||
@@ -53,10 +45,10 @@ class ListModeSelectDialog :
|
|||||||
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
|
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
|
||||||
binding.sliderGrid.addOnChangeListener(this)
|
binding.sliderGrid.addOnChangeListener(this)
|
||||||
|
|
||||||
binding.checkableGroup.onCheckedChangeListener = this
|
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
|
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
|
||||||
val mode = when (checkedId) {
|
val mode = when (checkedId) {
|
||||||
R.id.button_list -> ListMode.LIST
|
R.id.button_list -> ListMode.LIST
|
||||||
R.id.button_list_detailed -> ListMode.DETAILED_LIST
|
R.id.button_list_detailed -> ListMode.DETAILED_LIST
|
||||||
@@ -78,6 +70,6 @@ class ListModeSelectDialog :
|
|||||||
|
|
||||||
private const val TAG = "ListModeSelectDialog"
|
private const val TAG = "ListModeSelectDialog"
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
|
fun show(fm: FragmentManager) = ListModeBottomSheet().show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,10 @@ class MangaListMenuProvider(
|
|||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
R.id.action_list_mode -> {
|
R.id.action_list_mode -> {
|
||||||
ListModeSelectDialog.show(fragment.childFragmentManager)
|
ListModeBottomSheet.show(fragment.childFragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
@file:SuppressLint("UnsafeOptInUsageError")
|
|
||||||
|
|
||||||
package org.koitharu.kotatsu.list.ui.adapter
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
@@ -42,4 +39,4 @@ private fun initBadge(anchor: View): BadgeDrawable {
|
|||||||
private fun BadgeDrawable.align() {
|
private fun BadgeDrawable.align() {
|
||||||
horizontalOffset = intrinsicWidth
|
horizontalOffset = intrinsicWidth
|
||||||
verticalOffset = intrinsicHeight
|
verticalOffset = intrinsicHeight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
|
fun emptyHintAD(
|
||||||
|
listener: ListStateHolderListener,
|
||||||
|
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
|
||||||
|
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.icon.setImageResource(item.icon)
|
||||||
|
binding.textPrimary.setText(item.textPrimary)
|
||||||
|
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||||
|
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
import java.util.TreeSet
|
||||||
|
|
||||||
class FilterCoordinator(
|
class FilterCoordinator(
|
||||||
private val repository: RemoteMangaRepository,
|
private val repository: RemoteMangaRepository,
|
||||||
@@ -153,7 +160,7 @@ class FilterCoordinator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
repository.getTags()
|
repository.getTags()
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
error.printStackTraceDebug()
|
error.printStackTraceDebug()
|
||||||
@@ -204,4 +211,4 @@ class FilterCoordinator(
|
|||||||
return collator?.compare(t1, t2) ?: compareValues(t1, t2)
|
return collator?.compare(t1, t2) ?: compareValues(t1, t2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
class EmptyHint(
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
@StringRes textPrimary: Int,
|
||||||
|
@StringRes textSecondary: Int,
|
||||||
|
@StringRes actionStringRes: Int,
|
||||||
|
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) {
|
||||||
|
|
||||||
|
fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes)
|
||||||
|
}
|
||||||
@@ -7,28 +7,40 @@ import androidx.annotation.WorkerThread
|
|||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import java.io.File
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.util.*
|
import kotlinx.coroutines.Deferred
|
||||||
import java.util.zip.ZipEntry
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.util.zip.ZipFile
|
import kotlinx.coroutines.async
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.awaitAll
|
||||||
import javax.inject.Singleton
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filterNot
|
import kotlinx.coroutines.flow.filterNot
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.local.data.MangaIndex
|
import org.koitharu.kotatsu.local.data.MangaIndex
|
||||||
import org.koitharu.kotatsu.local.data.TempFileFilter
|
import org.koitharu.kotatsu.local.data.TempFileFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||||
import org.koitharu.kotatsu.utils.AlphanumComparator
|
import org.koitharu.kotatsu.utils.AlphanumComparator
|
||||||
import org.koitharu.kotatsu.utils.CompositeMutex
|
import org.koitharu.kotatsu.utils.CompositeMutex
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||||
import org.koitharu.kotatsu.utils.ext.readText
|
import org.koitharu.kotatsu.utils.ext.readText
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Enumeration
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
private const val MAX_PARALLELISM = 4
|
private const val MAX_PARALLELISM = 4
|
||||||
|
|
||||||
@@ -58,13 +70,14 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
|||||||
if (!tags.isNullOrEmpty()) {
|
if (!tags.isNullOrEmpty()) {
|
||||||
list.retainAll { x -> x.containsTags(tags) }
|
list.retainAll { x -> x.containsTags(tags) }
|
||||||
}
|
}
|
||||||
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
|
|
||||||
when (sortOrder) {
|
when (sortOrder) {
|
||||||
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
|
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
|
||||||
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
|
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
|
||||||
SortOrder.NEWEST,
|
SortOrder.NEWEST,
|
||||||
SortOrder.UPDATED,
|
SortOrder.UPDATED,
|
||||||
-> list.sortByDescending { it.createdAt }
|
-> list.sortByDescending { it.createdAt }
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
}
|
}
|
||||||
return list.unwrap()
|
return list.unwrap()
|
||||||
}
|
}
|
||||||
@@ -73,6 +86,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
|||||||
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) {
|
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) {
|
||||||
"Manga is not local or saved"
|
"Manga is not local or saved"
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> getFromFile(Uri.parse(manga.url).toFile())
|
else -> getFromFile(Uri.parse(manga.url).toFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +250,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
|
|||||||
context: CoroutineContext,
|
context: CoroutineContext,
|
||||||
): Deferred<LocalManga?> = async(context) {
|
): Deferred<LocalManga?> = async(context) {
|
||||||
runInterruptible {
|
runInterruptible {
|
||||||
runCatching { LocalManga(getFromFile(file), file) }.getOrNull()
|
runCatchingCancellable { LocalManga(getFromFile(file), file) }.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
|
|||||||
import org.koitharu.kotatsu.local.domain.importer.MangaImporter
|
import org.koitharu.kotatsu.local.domain.importer.MangaImporter
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.PendingIntentCompat
|
import org.koitharu.kotatsu.utils.PendingIntentCompat
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.asArrayList
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.report
|
||||||
|
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -48,8 +53,8 @@ class ImportService : CoroutineIntentService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun processIntent(intent: Intent?) {
|
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||||
val uris = intent?.getParcelableArrayListExtra<Uri>(EXTRA_URIS)
|
val uris = intent.getParcelableArrayListExtra<Uri>(EXTRA_URIS)
|
||||||
if (uris.isNullOrEmpty()) {
|
if (uris.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,10 @@ class ImportService : CoroutineIntentService() {
|
|||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onError(startId: Int, error: Throwable) {
|
||||||
|
error.report()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun importImpl(uri: Uri): Manga {
|
private suspend fun importImpl(uri: Uri): Manga {
|
||||||
val importer = importerFactory.create(uri)
|
val importer = importerFactory.create(uri)
|
||||||
return importer.import(uri)
|
return importer.import(uri)
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LocalChaptersRemoveService : CoroutineIntentService() {
|
class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||||
@@ -33,8 +35,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun processIntent(intent: Intent?) {
|
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||||
val manga = intent?.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
|
val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
|
||||||
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
|
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
|
||||||
startForeground()
|
startForeground()
|
||||||
val mangaWithChapters = localMangaRepository.getDetails(manga)
|
val mangaWithChapters = localMangaRepository.getDetails(manga)
|
||||||
@@ -46,6 +48,21 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
|||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onError(startId: Int, error: Throwable) {
|
||||||
|
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle(getString(R.string.error_occurred))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setDefaults(0)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.blue_primary_dark))
|
||||||
|
.setSilent(true)
|
||||||
|
.setContentText(error.getDisplayMessage(resources))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build()
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID + startId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
private fun startForeground() {
|
private fun startForeground() {
|
||||||
val title = getString(R.string.local_manga_processing)
|
val title = getString(R.string.local_manga_processing)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LocalListViewModel @Inject constructor(
|
class LocalListViewModel @Inject constructor(
|
||||||
@@ -63,6 +64,7 @@ class LocalListViewModel @Inject constructor(
|
|||||||
actionStringRes = R.string._import,
|
actionStringRes = R.string._import,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> buildList(list.size + 1) {
|
else -> buildList(list.size + 1) {
|
||||||
add(createHeader(list, tags, order))
|
add(createHeader(list, tags, order))
|
||||||
list.toUi(this, mode, this@LocalListViewModel)
|
list.toUi(this, mode, this@LocalListViewModel)
|
||||||
@@ -104,7 +106,7 @@ class LocalListViewModel @Inject constructor(
|
|||||||
for (manga in itemsToRemove) {
|
for (manga in itemsToRemove) {
|
||||||
val original = repository.getRemoteManga(manga)
|
val original = repository.getRemoteManga(manga)
|
||||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
repository.delete(manga) || throw IOException("Unable to delete file")
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
historyRepository.deleteOrSwap(manga, original)
|
||||||
}
|
}
|
||||||
mangaList.update { list ->
|
mangaList.update { list ->
|
||||||
@@ -120,6 +122,8 @@ class LocalListViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
listError.value = null
|
listError.value = null
|
||||||
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
listError.value = e
|
listError.value = e
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,7 @@ class LocalListViewModel @Inject constructor(
|
|||||||
private fun cleanup() {
|
private fun cleanup() {
|
||||||
if (!DownloadService.isRunning && !ImportService.isRunning && !LocalChaptersRemoveService.isRunning) {
|
if (!DownloadService.isRunning && !ImportService.isRunning && !LocalChaptersRemoveService.isRunning) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
repository.cleanup()
|
repository.cleanup()
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
error.printStackTraceDebug()
|
error.printStackTraceDebug()
|
||||||
|
|||||||
@@ -15,15 +15,21 @@ import androidx.core.app.ActivityOptionsCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.util.size
|
import androidx.core.util.size
|
||||||
import androidx.core.view.*
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
||||||
|
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
|
||||||
|
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
|
||||||
|
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
|
||||||
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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -34,7 +40,6 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
|
|||||||
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.library.ui.LibraryFragment
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -48,10 +53,18 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
|||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
|
||||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
import org.koitharu.kotatsu.utils.VoiceInputContract
|
import org.koitharu.kotatsu.utils.VoiceInputContract
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.drawableEnd
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.hideKeyboard
|
||||||
|
import org.koitharu.kotatsu.utils.ext.resolve
|
||||||
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.tryLaunch
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
private const val TAG_SEARCH = "search"
|
private const val TAG_SEARCH = "search"
|
||||||
|
|
||||||
@@ -115,6 +128,7 @@ class MainActivity :
|
|||||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||||
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
|
||||||
viewModel.counters.observe(this, ::onCountersChanged)
|
viewModel.counters.observe(this, ::onCountersChanged)
|
||||||
|
viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
@@ -127,20 +141,18 @@ class MainActivity :
|
|||||||
binding.searchView.clearFocus()
|
binding.searchView.clearFocus()
|
||||||
when {
|
when {
|
||||||
fragment != null -> supportFragmentManager.commit {
|
fragment != null -> supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
remove(fragment)
|
remove(fragment)
|
||||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
runOnCommit { onSearchClosed() }
|
runOnCommit { onSearchClosed() }
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onBackPressed()
|
else -> super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
||||||
if (fragment is LibraryFragment) {
|
adjustFabVisibility(topFragment = fragment)
|
||||||
binding.fab?.show()
|
|
||||||
} else {
|
|
||||||
binding.fab?.hide()
|
|
||||||
}
|
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
binding.appbar.setExpanded(true)
|
binding.appbar.setExpanded(true)
|
||||||
}
|
}
|
||||||
@@ -173,6 +185,7 @@ class MainActivity :
|
|||||||
if (v?.id == R.id.searchView && hasFocus) {
|
if (v?.id == R.id.searchView && hasFocus) {
|
||||||
if (fragment == null) {
|
if (fragment == null) {
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
|
add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
|
||||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
runOnCommit { onSearchOpened() }
|
runOnCommit { onSearchOpened() }
|
||||||
@@ -257,6 +270,10 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onFeedAvailabilityChanged(isFeedAvailable: Boolean) {
|
||||||
|
navigationDelegate.setItemVisibility(R.id.nav_feed, isFeedAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
binding.fab?.isEnabled = !isLoading
|
binding.fab?.isEnabled = !isLoading
|
||||||
}
|
}
|
||||||
@@ -308,18 +325,18 @@ class MainActivity :
|
|||||||
topFragment: Fragment? = navigationDelegate.primaryFragment,
|
topFragment: Fragment? = navigationDelegate.primaryFragment,
|
||||||
isSearchOpened: Boolean = isSearchOpened(),
|
isSearchOpened: Boolean = isSearchOpened(),
|
||||||
) {
|
) {
|
||||||
val fab = binding.fab
|
val fab = binding.fab ?: return
|
||||||
if (
|
if (
|
||||||
isResumeEnabled &&
|
isResumeEnabled &&
|
||||||
!actionModeDelegate.isActionModeStarted &&
|
!actionModeDelegate.isActionModeStarted &&
|
||||||
!isSearchOpened &&
|
!isSearchOpened &&
|
||||||
topFragment is LibraryFragment
|
topFragment is ShelfFragment
|
||||||
) {
|
) {
|
||||||
if (fab?.isVisible == false) {
|
if (!fab.isVisible) {
|
||||||
fab.show()
|
fab.show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (fab?.isVisible == true) {
|
if (fab.isVisible) {
|
||||||
fab.hide()
|
fab.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.main.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.core.view.iterator
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
@@ -10,10 +11,10 @@ import com.google.android.material.navigation.NavigationBarView
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||||
import org.koitharu.kotatsu.explore.ui.ExploreFragment
|
import org.koitharu.kotatsu.explore.ui.ExploreFragment
|
||||||
import org.koitharu.kotatsu.library.ui.LibraryFragment
|
|
||||||
import org.koitharu.kotatsu.settings.tools.ToolsFragment
|
import org.koitharu.kotatsu.settings.tools.ToolsFragment
|
||||||
import org.koitharu.kotatsu.tracker.ui.FeedFragment
|
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
|
||||||
import java.util.*
|
import org.koitharu.kotatsu.tracker.ui.feed.FeedFragment
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
private const val TAG_PRIMARY = "primary"
|
private const val TAG_PRIMARY = "primary"
|
||||||
|
|
||||||
@@ -62,6 +63,14 @@ class MainNavigationDelegate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setItemVisibility(@IdRes itemId: Int, isVisible: Boolean) {
|
||||||
|
val item = navBar.menu.findItem(itemId) ?: return
|
||||||
|
item.isVisible = isVisible
|
||||||
|
if (item.isChecked && !isVisible) {
|
||||||
|
navBar.selectedItemId = firstItem()?.itemId ?: return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addOnFragmentChangedListener(listener: OnFragmentChangedListener) {
|
fun addOnFragmentChangedListener(listener: OnFragmentChangedListener) {
|
||||||
listeners.add(listener)
|
listeners.add(listener)
|
||||||
}
|
}
|
||||||
@@ -73,7 +82,7 @@ class MainNavigationDelegate(
|
|||||||
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
|
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
|
||||||
setPrimaryFragment(
|
setPrimaryFragment(
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.nav_library -> LibraryFragment.newInstance()
|
R.id.nav_shelf -> ShelfFragment.newInstance()
|
||||||
R.id.nav_explore -> ExploreFragment.newInstance()
|
R.id.nav_explore -> ExploreFragment.newInstance()
|
||||||
R.id.nav_feed -> FeedFragment.newInstance()
|
R.id.nav_feed -> FeedFragment.newInstance()
|
||||||
R.id.nav_tools -> ToolsFragment.newInstance()
|
R.id.nav_tools -> ToolsFragment.newInstance()
|
||||||
@@ -85,6 +94,7 @@ class MainNavigationDelegate(
|
|||||||
|
|
||||||
private fun setPrimaryFragment(fragment: Fragment) {
|
private fun setPrimaryFragment(fragment: Fragment) {
|
||||||
fragmentManager.beginTransaction()
|
fragmentManager.beginTransaction()
|
||||||
|
.setReorderingAllowed(true)
|
||||||
.replace(R.id.container, fragment, TAG_PRIMARY)
|
.replace(R.id.container, fragment, TAG_PRIMARY)
|
||||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
.commit()
|
.commit()
|
||||||
@@ -95,6 +105,14 @@ class MainNavigationDelegate(
|
|||||||
listeners.forEach { it.onFragmentChanged(fragment, fromUser) }
|
listeners.forEach { it.onFragmentChanged(fragment, fromUser) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun firstItem(): MenuItem? {
|
||||||
|
val menu = navBar.menu
|
||||||
|
for (item in menu) {
|
||||||
|
if (item.isVisible) return item
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
interface OnFragmentChangedListener {
|
interface OnFragmentChangedListener {
|
||||||
|
|
||||||
fun onFragmentChanged(fragment: Fragment, fromUser: Boolean)
|
fun onFragmentChanged(fragment: Fragment, fromUser: Boolean)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.util.SparseIntArray
|
|||||||
import androidx.core.util.set
|
import androidx.core.util.set
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -12,6 +11,8 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
@@ -19,6 +20,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
@@ -27,6 +29,7 @@ class MainViewModel @Inject constructor(
|
|||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
syncController: SyncController,
|
syncController: SyncController,
|
||||||
database: MangaDatabase,
|
database: MangaDatabase,
|
||||||
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onOpenReader = SingleLiveEvent<Manga>()
|
val onOpenReader = SingleLiveEvent<Manga>()
|
||||||
@@ -35,6 +38,12 @@ class MainViewModel @Inject constructor(
|
|||||||
.observeHasItems()
|
.observeHasItems()
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||||
|
|
||||||
|
val isFeedAvailable = settings.observeAsLiveData(
|
||||||
|
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_TRACKER_ENABLED,
|
||||||
|
valueProducer = { isTrackerEnabled },
|
||||||
|
)
|
||||||
|
|
||||||
val counters = combine(
|
val counters = combine(
|
||||||
appUpdateRepository.observeAvailableUpdate(),
|
appUpdateRepository.observeAvailableUpdate(),
|
||||||
trackingRepository.observeUpdatedMangaCount(),
|
trackingRepository.observeUpdatedMangaCount(),
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.main.ui.protect
|
package org.koitharu.kotatsu.main.ui.protect
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import org.acra.dialog.CrashReportDialog
|
||||||
|
import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import org.acra.dialog.CrashReportDialog
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AppProtectHelper @Inject constructor(private val settings: AppSettings) : Application.ActivityLifecycleCallbacks {
|
class AppProtectHelper @Inject constructor(private val settings: AppSettings) : DefaultActivityLifecycleCallbacks {
|
||||||
|
|
||||||
private var isUnlocked = settings.appPassword.isNullOrEmpty()
|
private var isUnlocked = settings.appPassword.isNullOrEmpty()
|
||||||
|
|
||||||
@@ -27,16 +27,6 @@ class AppProtectHelper @Inject constructor(private val settings: AppSettings) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityStarted(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityStopped(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
||||||
|
|
||||||
override fun onActivityDestroyed(activity: Activity) {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
if (activity !is ProtectActivity && activity.isFinishing && activity.isTaskRoot) {
|
if (activity !is ProtectActivity && activity.isFinishing && activity.isTaskRoot) {
|
||||||
restoreLock()
|
restoreLock()
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ class ProtectActivity :
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
if (!useFingerprint()) {
|
if (!useFingerprint()) {
|
||||||
binding.editPassword.requestFocus()
|
binding.editPassword.requestFocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import android.net.Uri
|
|||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.util.zip.ZipFile
|
import kotlinx.coroutines.Dispatchers
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -30,7 +32,15 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||||
|
import java.io.File
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
private const val PROGRESS_UNDEFINED = -1f
|
private const val PROGRESS_UNDEFINED = -1f
|
||||||
private const val PREFETCH_LIMIT_DEFAULT = 10
|
private const val PREFETCH_LIMIT_DEFAULT = 10
|
||||||
@@ -43,7 +53,7 @@ class PageLoader @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
|
|
||||||
val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default)
|
||||||
|
|
||||||
private val connectivityManager = context.connectivityManager
|
private val connectivityManager = context.connectivityManager
|
||||||
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
||||||
@@ -197,4 +207,13 @@ class PageLoader @Inject constructor(
|
|||||||
val deferred = CompletableDeferred(file)
|
val deferred = CompletableDeferred(file)
|
||||||
return ProgressDeferred(deferred, emptyProgressFlow)
|
return ProgressDeferred(deferred, emptyProgressFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||||
|
CoroutineExceptionHandler {
|
||||||
|
|
||||||
|
override fun handleException(context: CoroutineContext, exception: Throwable) {
|
||||||
|
exception.printStackTraceDebug()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,23 @@ import android.transition.Fade
|
|||||||
import android.transition.Slide
|
import android.transition.Slide
|
||||||
import android.transition.TransitionManager
|
import android.transition.TransitionManager
|
||||||
import android.transition.TransitionSet
|
import android.transition.TransitionSet
|
||||||
import android.view.*
|
import android.view.Gravity
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.*
|
import androidx.core.view.OnApplyWindowInsetsListener
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
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 java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -42,7 +50,17 @@ import org.koitharu.kotatsu.settings.SettingsActivity
|
|||||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||||
import org.koitharu.kotatsu.utils.IdlingDetector
|
import org.koitharu.kotatsu.utils.IdlingDetector
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.hasGlobalPoint
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isReportable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
|
||||||
|
import org.koitharu.kotatsu.utils.ext.postDelayed
|
||||||
|
import org.koitharu.kotatsu.utils.ext.report
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ReaderActivity :
|
class ReaderActivity :
|
||||||
@@ -145,6 +163,7 @@ class ReaderActivity :
|
|||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
startActivity(SettingsActivity.newReaderSettingsIntent(this))
|
startActivity(SettingsActivity.newReaderSettingsIntent(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_chapters -> {
|
R.id.action_chapters -> {
|
||||||
ChaptersBottomSheet.show(
|
ChaptersBottomSheet.show(
|
||||||
supportFragmentManager,
|
supportFragmentManager,
|
||||||
@@ -152,6 +171,7 @@ class ReaderActivity :
|
|||||||
viewModel.getCurrentState()?.chapterId ?: 0L,
|
viewModel.getCurrentState()?.chapterId ?: 0L,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_pages_thumbs -> {
|
R.id.action_pages_thumbs -> {
|
||||||
val pages = viewModel.getCurrentChapterPages()
|
val pages = viewModel.getCurrentChapterPages()
|
||||||
if (!pages.isNullOrEmpty()) {
|
if (!pages.isNullOrEmpty()) {
|
||||||
@@ -165,6 +185,7 @@ class ReaderActivity :
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_bookmark -> {
|
R.id.action_bookmark -> {
|
||||||
if (viewModel.isBookmarkAdded.value == true) {
|
if (viewModel.isBookmarkAdded.value == true) {
|
||||||
viewModel.removeBookmark()
|
viewModel.removeBookmark()
|
||||||
@@ -172,11 +193,13 @@ class ReaderActivity :
|
|||||||
viewModel.addBookmark()
|
viewModel.addBookmark()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_options -> {
|
R.id.action_options -> {
|
||||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
||||||
val currentMode = readerManager.currentMode ?: return false
|
val currentMode = readerManager.currentMode ?: return false
|
||||||
ReaderConfigBottomSheet.show(supportFragmentManager, currentMode)
|
ReaderConfigBottomSheet.show(supportFragmentManager, currentMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -364,9 +387,9 @@ class ReaderActivity :
|
|||||||
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
|
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (uiState.totalPages > 1 && uiState.currentPage < uiState.totalPages) {
|
if (uiState.isSliderAvailable()) {
|
||||||
binding.slider.valueTo = uiState.totalPages.toFloat() - 1
|
binding.slider.valueTo = uiState.totalPages.toFloat() - 1
|
||||||
binding.slider.value = uiState.currentPage.toFloat()
|
binding.slider.setValueRounded(uiState.currentPage.toFloat())
|
||||||
binding.slider.isVisible = true
|
binding.slider.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
binding.slider.isVisible = false
|
binding.slider.isVisible = false
|
||||||
@@ -383,7 +406,7 @@ class ReaderActivity :
|
|||||||
if (ExceptionResolver.canResolve(exception)) {
|
if (ExceptionResolver.canResolve(exception)) {
|
||||||
tryResolve(exception)
|
tryResolve(exception)
|
||||||
} else {
|
} else {
|
||||||
exception.report("ReaderActivity::onError")
|
exception.report()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onCancel(dialog)
|
onCancel(dialog)
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
|
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
|
||||||
import org.koitharu.kotatsu.databinding.DialogReaderConfigBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
@Deprecated("Not in use")
|
|
||||||
class ReaderConfigDialog :
|
|
||||||
AlertDialogFragment<DialogReaderConfigBinding>(),
|
|
||||||
CheckableButtonGroup.OnCheckedChangeListener {
|
|
||||||
|
|
||||||
private lateinit var mode: ReaderMode
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = DialogReaderConfigBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
mode = arguments?.getInt(ARG_MODE)
|
|
||||||
?.let { ReaderMode.valueOf(it) }
|
|
||||||
?: ReaderMode.STANDARD
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
|
||||||
return super.onBuildDialog(builder)
|
|
||||||
.setTitle(R.string.read_mode)
|
|
||||||
.setPositiveButton(R.string.done, null)
|
|
||||||
.setCancelable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD
|
|
||||||
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
|
|
||||||
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
|
|
||||||
|
|
||||||
binding.checkableGroup.onCheckedChangeListener = this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
(
|
|
||||||
(parentFragment as? Callback)
|
|
||||||
?: (activity as? Callback)
|
|
||||||
)?.onReaderModeChanged(mode)
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
|
|
||||||
mode = when (checkedId) {
|
|
||||||
R.id.button_standard -> ReaderMode.STANDARD
|
|
||||||
R.id.button_webtoon -> ReaderMode.WEBTOON
|
|
||||||
R.id.button_reversed -> ReaderMode.REVERSED
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
|
|
||||||
fun onReaderModeChanged(mode: ReaderMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "ReaderConfigDialog"
|
|
||||||
private const val ARG_MODE = "mode"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigDialog().withArgs(1) {
|
|
||||||
putInt(ARG_MODE, mode.id)
|
|
||||||
}.show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -62,8 +62,9 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
|||||||
init {
|
init {
|
||||||
paint.strokeWidth = context.resources.resolveDp(2f)
|
paint.strokeWidth = context.resources.resolveDp(2f)
|
||||||
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
|
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
|
||||||
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start") + insetCorner
|
val fallbackInset = resources.getDimensionPixelOffset(R.dimen.reader_bar_inset_fallback)
|
||||||
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end") + insetCorner
|
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start", fallbackInset) + insetCorner
|
||||||
|
val insetEnd = getSystemUiDimensionOffset("status_bar_padding_end", fallbackInset) + insetCorner
|
||||||
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
|
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
|
||||||
insetLeft = if (isRtl) insetEnd else insetStart
|
insetLeft = if (isRtl) insetEnd else insetStart
|
||||||
insetRight = if (isRtl) insetStart else insetEnd
|
insetRight = if (isRtl) insetStart else insetEnd
|
||||||
@@ -179,12 +180,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSystemUiDimensionOffset(name: String): Int = runCatching {
|
private fun getSystemUiDimensionOffset(name: String, fallback: Int = 0): Int = runCatching {
|
||||||
val manager = context.packageManager
|
val manager = context.packageManager
|
||||||
val resources = manager.getResourcesForApplication("com.android.systemui")
|
val resources = manager.getResourcesForApplication("com.android.systemui")
|
||||||
val resId = resources.getIdentifier(name, "dimen", "com.android.systemui")
|
val resId = resources.getIdentifier(name, "dimen", "com.android.systemui")
|
||||||
resources.getDimensionPixelOffset(resId)
|
resources.getDimensionPixelOffset(resId)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
}.getOrDefault(0)
|
}.getOrDefault(fallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
|||||||
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
||||||
import java.util.*
|
import java.util.EnumMap
|
||||||
|
|
||||||
class ReaderManager(
|
class ReaderManager(
|
||||||
private val fragmentManager: FragmentManager,
|
private val fragmentManager: FragmentManager,
|
||||||
@@ -35,11 +35,15 @@ class ReaderManager(
|
|||||||
fun replace(newMode: ReaderMode) {
|
fun replace(newMode: ReaderMode) {
|
||||||
val readerClass = requireNotNull(modeMap[newMode])
|
val readerClass = requireNotNull(modeMap[newMode])
|
||||||
fragmentManager.commit {
|
fragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
replace(containerResId, readerClass, null, null)
|
replace(containerResId, readerClass, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replace(reader: BaseReader<*>) {
|
fun replace(reader: BaseReader<*>) {
|
||||||
fragmentManager.commit { replace(containerResId, reader) }
|
fragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
|
replace(containerResId, reader)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,22 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import java.util.*
|
import kotlinx.coroutines.CancellationException
|
||||||
import javax.inject.Provider
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||||
@@ -21,7 +33,11 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.*
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
@@ -38,6 +54,9 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
|
|||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
import org.koitharu.kotatsu.utils.ext.requireValue
|
import org.koitharu.kotatsu.utils.ext.requireValue
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import java.util.Date
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
private const val BOUNDS_PAGE_OFFSET = 2
|
private const val BOUNDS_PAGE_OFFSET = 2
|
||||||
private const val PREFETCH_LIMIT = 10
|
private const val PREFETCH_LIMIT = 10
|
||||||
@@ -87,6 +106,12 @@ class ReaderViewModel @AssistedInject constructor(
|
|||||||
valueProducer = { isReaderBarEnabled },
|
valueProducer = { isReaderBarEnabled },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val isWebtoonZoomEnabled = settings.observeAsLiveData(
|
||||||
|
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_WEBTOON_ZOOM,
|
||||||
|
valueProducer = { isWebtoonZoomEnable },
|
||||||
|
)
|
||||||
|
|
||||||
val readerSettings = ReaderSettings(
|
val readerSettings = ReaderSettings(
|
||||||
parentScope = viewModelScope,
|
parentScope = viewModelScope,
|
||||||
settings = settings,
|
settings = settings,
|
||||||
@@ -115,6 +140,10 @@ class ReaderViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
loadImpl()
|
loadImpl()
|
||||||
|
settings.observe()
|
||||||
|
.onEach { key ->
|
||||||
|
if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged()
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@@ -326,7 +355,7 @@ class ReaderViewModel @AssistedInject constructor(
|
|||||||
?: manga.chapters?.randomOrNull()
|
?: manga.chapters?.randomOrNull()
|
||||||
?: error("There are no chapters in this manga")
|
?: error("There are no chapters in this manga")
|
||||||
val pages = repo.getPages(chapter)
|
val pages = repo.getPages(chapter)
|
||||||
return runCatching {
|
return runCatchingCancellable {
|
||||||
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
|
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
|
||||||
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
|
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
@@ -347,6 +376,7 @@ class ReaderViewModel @AssistedInject constructor(
|
|||||||
chaptersTotal = chapters.size(),
|
chaptersTotal = chapters.size(),
|
||||||
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
||||||
currentPage = state?.page ?: 0,
|
currentPage = state?.page ?: 0,
|
||||||
|
isSliderEnabled = settings.isReaderSliderEnabled,
|
||||||
)
|
)
|
||||||
uiState.postValue(newState)
|
uiState.postValue(newState)
|
||||||
}
|
}
|
||||||
@@ -381,7 +411,7 @@ class ReaderViewModel @AssistedInject constructor(
|
|||||||
*/
|
*/
|
||||||
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
|
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
|
||||||
return processLifecycleScope.launch(Dispatchers.Default) {
|
return processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
addOrUpdate(
|
addOrUpdate(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
chapterId = state.chapterId,
|
chapterId = state.chapterId,
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ class ColorFilterConfigActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
|
private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) {
|
||||||
binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f
|
binding.sliderBrightness.setValueRounded(readerColorFilter?.brightness ?: 0f)
|
||||||
binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f
|
binding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f)
|
||||||
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
|
binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
|||||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class ReaderConfigBottomSheet :
|
|||||||
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
|
binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources))
|
||||||
|
|
||||||
findCallback()?.run {
|
findCallback()?.run {
|
||||||
binding.sliderTimer.value = pageSwitchDelay
|
binding.sliderTimer.setValueRounded(pageSwitchDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +76,16 @@ class ReaderConfigBottomSheet :
|
|||||||
startActivity(SettingsActivity.newReaderSettingsIntent(v.context))
|
startActivity(SettingsActivity.newReaderSettingsIntent(v.context))
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_save_page -> {
|
R.id.button_save_page -> {
|
||||||
val page = viewModel.getCurrentPage() ?: return
|
val page = viewModel.getCurrentPage() ?: return
|
||||||
viewModel.saveCurrentPage(page, savePageRequest)
|
viewModel.saveCurrentPage(page, savePageRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_screen_rotate -> {
|
R.id.button_screen_rotate -> {
|
||||||
orientationHelper?.toggleOrientation()
|
orientationHelper?.toggleOrientation()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_color_filter -> {
|
R.id.button_color_filter -> {
|
||||||
val page = viewModel.getCurrentPage() ?: return
|
val page = viewModel.getCurrentPage() ?: return
|
||||||
val manga = viewModel.manga ?: return
|
val manga = viewModel.manga ?: return
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package org.koitharu.kotatsu.reader.ui.config
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
||||||
@@ -60,7 +64,7 @@ class ReaderSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
if (key == AppSettings.KEY_ZOOM_MODE || key == AppSettings.KEY_PAGES_NUMBERS) {
|
if (key == AppSettings.KEY_ZOOM_MODE || key == AppSettings.KEY_PAGES_NUMBERS || key == AppSettings.KEY_WEBTOON_ZOOM) {
|
||||||
notifyChanged()
|
notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,23 @@ import android.net.Uri
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||||
import java.io.File
|
import kotlinx.coroutines.CancellationException
|
||||||
import java.io.IOException
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class PageHolderDelegate(
|
class PageHolderDelegate(
|
||||||
private val loader: PageLoader,
|
private val loader: PageLoader,
|
||||||
@@ -102,6 +108,8 @@ class PageHolderDelegate(
|
|||||||
loader.convertInPlace(file)
|
loader.convertInPlace(file)
|
||||||
state = State.CONVERTED
|
state = State.CONVERTED
|
||||||
callback.onImageReady(file.toUri())
|
callback.onImageReady(file.toUri())
|
||||||
|
} catch (ce: CancellationException) {
|
||||||
|
throw ce
|
||||||
} catch (e2: Throwable) {
|
} catch (e2: Throwable) {
|
||||||
e.addSuppressed(e2)
|
e.addSuppressed(e2)
|
||||||
state = State.ERROR
|
state = State.ERROR
|
||||||
@@ -124,7 +132,7 @@ class PageHolderDelegate(
|
|||||||
callback.onImageReady(file.toUri())
|
callback.onImageReady(file.toUri())
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
state = State.ERROR
|
state = State.ERROR
|
||||||
error = e
|
error = e
|
||||||
callback.onError(e)
|
callback.onError(e)
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ data class ReaderUiState(
|
|||||||
val chaptersTotal: Int,
|
val chaptersTotal: Int,
|
||||||
val currentPage: Int,
|
val currentPage: Int,
|
||||||
val totalPages: Int,
|
val totalPages: Int,
|
||||||
|
private val isSliderEnabled: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun isSliderAvailable(): Boolean {
|
||||||
|
return isSliderEnabled && totalPages > 1 && currentPage < totalPages
|
||||||
|
}
|
||||||
|
|
||||||
fun computePercent(): Float {
|
fun computePercent(): Float {
|
||||||
val ppc = 1f / chaptersTotal
|
val ppc = 1f / chaptersTotal
|
||||||
val chapterIndex = chapterNumber - 1
|
val chapterIndex = chapterNumber - 1
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
|||||||
adapter = webtoonAdapter
|
adapter = webtoonAdapter
|
||||||
addOnPageScrollListener(PageScrollListener())
|
addOnPageScrollListener(PageScrollListener())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.isWebtoonZoomEnabled.observe(viewLifecycleOwner) {
|
||||||
|
binding.frame.isZoomEnable = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.OverScroller
|
||||||
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
|
||||||
|
private const val MAX_SCALE = 2.5f
|
||||||
|
private const val MIN_SCALE = 1f // under-scaling disabled due to buggy nested scroll
|
||||||
|
|
||||||
|
class WebtoonScalingFrame @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyles: Int = 0,
|
||||||
|
) : FrameLayout(context, attrs, defStyles), ScaleGestureDetector.OnScaleGestureListener {
|
||||||
|
|
||||||
|
private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) }
|
||||||
|
|
||||||
|
private val scaleDetector = ScaleGestureDetector(context, this)
|
||||||
|
private val gestureDetector = GestureDetectorCompat(context, GestureListener())
|
||||||
|
private val overScroller = OverScroller(context, AccelerateDecelerateInterpolator())
|
||||||
|
private val transformMatrix = Matrix()
|
||||||
|
private val matrixValues = FloatArray(9)
|
||||||
|
private val scale
|
||||||
|
get() = matrixValues[Matrix.MSCALE_X]
|
||||||
|
private val transX
|
||||||
|
get() = halfWidth * (scale - 1f) + matrixValues[Matrix.MTRANS_X]
|
||||||
|
private val transY
|
||||||
|
get() = halfHeight * (scale - 1f) + matrixValues[Matrix.MTRANS_Y]
|
||||||
|
private var halfWidth = 0f
|
||||||
|
private var halfHeight = 0f
|
||||||
|
private val translateBounds = RectF()
|
||||||
|
private val targetHitRect = Rect()
|
||||||
|
private var pendingScroll = 0
|
||||||
|
|
||||||
|
var isZoomEnable = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (scale != 1f) {
|
||||||
|
scaleChild(1f, halfWidth, halfHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
syncMatrixValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
|
if (!isZoomEnable || ev == null) {
|
||||||
|
return super.dispatchTouchEvent(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.action == MotionEvent.ACTION_DOWN && overScroller.computeScrollOffset()) {
|
||||||
|
overScroller.forceFinished(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
gestureDetector.onTouchEvent(ev)
|
||||||
|
scaleDetector.onTouchEvent(ev)
|
||||||
|
|
||||||
|
// Offset event to inside the child view
|
||||||
|
if (scale < 1 && !targetHitRect.contains(ev.x.toInt(), ev.y.toInt())) {
|
||||||
|
ev.offsetLocation(halfWidth - ev.x + targetHitRect.width() / 3, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send action cancel to avoid recycler jump when scale end
|
||||||
|
if (scaleDetector.isInProgress) {
|
||||||
|
ev.action = MotionEvent.ACTION_CANCEL
|
||||||
|
}
|
||||||
|
return super.dispatchTouchEvent(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
halfWidth = measuredWidth / 2f
|
||||||
|
halfHeight = measuredHeight / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateTarget() {
|
||||||
|
adjustBounds()
|
||||||
|
targetChild.run {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
translationX = transX
|
||||||
|
translationY = transY
|
||||||
|
}
|
||||||
|
|
||||||
|
val newHeight = if (scale < 1f) (height / scale).toInt() else height
|
||||||
|
if (newHeight != targetChild.height) {
|
||||||
|
targetChild.layoutParams.height = newHeight
|
||||||
|
targetChild.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale < 1) {
|
||||||
|
targetChild.getHitRect(targetHitRect)
|
||||||
|
targetChild.scrollBy(0, pendingScroll)
|
||||||
|
pendingScroll = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncMatrixValues() {
|
||||||
|
transformMatrix.getValues(matrixValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustBounds() {
|
||||||
|
syncMatrixValues()
|
||||||
|
val dx = when {
|
||||||
|
transX < translateBounds.left -> translateBounds.left - transX
|
||||||
|
transX > translateBounds.right -> translateBounds.right - transX
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
val dy = when {
|
||||||
|
transY < translateBounds.top -> translateBounds.top - transY
|
||||||
|
transY > translateBounds.bottom -> translateBounds.bottom - transY
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingScroll = dy.toInt()
|
||||||
|
transformMatrix.postTranslate(dx, dy)
|
||||||
|
syncMatrixValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scaleChild(newScale: Float, focusX: Float, focusY: Float) {
|
||||||
|
val factor = newScale / scale
|
||||||
|
if (newScale > 1) {
|
||||||
|
translateBounds.set(
|
||||||
|
halfWidth * (1 - newScale),
|
||||||
|
halfHeight * (1 - newScale),
|
||||||
|
halfWidth * (newScale - 1),
|
||||||
|
halfHeight * (newScale - 1),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
translateBounds.set(
|
||||||
|
0f,
|
||||||
|
halfHeight - halfHeight / newScale,
|
||||||
|
0f,
|
||||||
|
halfHeight - halfHeight / newScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
transformMatrix.postScale(factor, factor, focusX, focusY)
|
||||||
|
invalidateTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
|
val newScale = (scale * detector.scaleFactor).coerceIn(MIN_SCALE, MAX_SCALE)
|
||||||
|
scaleChild(newScale, detector.focusX, detector.focusY)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
|
||||||
|
|
||||||
|
override fun onScaleEnd(p0: ScaleGestureDetector) {
|
||||||
|
pendingScroll = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
|
||||||
|
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
||||||
|
if (scale <= 1f) return false
|
||||||
|
transformMatrix.postTranslate(-distanceX, -distanceY)
|
||||||
|
invalidateTarget()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
|
val newScale = if (scale != 1f) 1f else MAX_SCALE * 0.8f
|
||||||
|
ObjectAnimator.ofFloat(scale, newScale).run {
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
duration = 300
|
||||||
|
addUpdateListener {
|
||||||
|
scaleChild(it.animatedValue as Float, e.x, e.y)
|
||||||
|
}
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
||||||
|
if (scale <= 1) return false
|
||||||
|
|
||||||
|
overScroller.fling(
|
||||||
|
transX.toInt(),
|
||||||
|
transY.toInt(),
|
||||||
|
velocityX.toInt(),
|
||||||
|
velocityY.toInt(),
|
||||||
|
translateBounds.left.toInt(),
|
||||||
|
translateBounds.right.toInt(),
|
||||||
|
translateBounds.top.toInt(),
|
||||||
|
translateBounds.bottom.toInt(),
|
||||||
|
)
|
||||||
|
postOnAnimation(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
if (overScroller.computeScrollOffset()) {
|
||||||
|
transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY)
|
||||||
|
invalidateTarget()
|
||||||
|
postOnAnimation(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|||||||
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
||||||
|
|
||||||
fun pageThumbnailAD(
|
fun pageThumbnailAD(
|
||||||
@@ -72,7 +73,7 @@ fun pageThumbnailAD(
|
|||||||
text = (item.number).toString()
|
text = (item.number).toString()
|
||||||
}
|
}
|
||||||
job = scope.launch {
|
job = scope.launch {
|
||||||
val drawable = runCatching {
|
val drawable = runCatchingCancellable {
|
||||||
loadPageThumbnail(item)
|
loadPageThumbnail(item)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
binding.imageViewThumb.setImageDrawable(drawable)
|
binding.imageViewThumb.setImageDrawable(drawable)
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import java.util.*
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
@@ -21,13 +26,20 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator
|
|||||||
import org.koitharu.kotatsu.list.ui.filter.FilterItem
|
import org.koitharu.kotatsu.list.ui.filter.FilterItem
|
||||||
import org.koitharu.kotatsu.list.ui.filter.FilterState
|
import org.koitharu.kotatsu.list.ui.filter.FilterState
|
||||||
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
private const val FILTER_MIN_INTERVAL = 250L
|
private const val FILTER_MIN_INTERVAL = 250L
|
||||||
|
|
||||||
@@ -138,6 +150,8 @@ class RemoteListViewModel @AssistedInject constructor(
|
|||||||
mangaList.value = mangaList.value?.plus(list) ?: list
|
mangaList.value = mangaList.value?.plus(list) ?: list
|
||||||
}
|
}
|
||||||
hasNextPage.value = list.isNotEmpty()
|
hasNextPage.value = list.isNotEmpty()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
listError.value = e
|
listError.value = e
|
||||||
@@ -149,7 +163,7 @@ class RemoteListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createEmptyState(canResetFilter: Boolean) = EmptyState(
|
private fun createEmptyState(canResetFilter: Boolean) = EmptyState(
|
||||||
icon = R.drawable.ic_empty_search,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = 0,
|
textSecondary = 0,
|
||||||
actionStringRes = if (canResetFilter) R.string.reset_filter else 0,
|
actionStringRes = if (canResetFilter) R.string.reset_filter else 0,
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain
|
|||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.collection.getOrElse
|
import androidx.collection.getOrElse
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
import java.util.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.*
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||||
import org.koitharu.kotatsu.utils.ext.findKeyByValue
|
import org.koitharu.kotatsu.utils.ext.findKeyByValue
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import java.util.EnumMap
|
||||||
|
|
||||||
abstract class Scrobbler(
|
abstract class Scrobbler(
|
||||||
protected val db: MangaDatabase,
|
protected val db: MangaDatabase,
|
||||||
@@ -47,7 +52,7 @@ abstract class Scrobbler(
|
|||||||
|
|
||||||
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
|
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
|
||||||
val mangaInfo = infoCache.getOrElse(targetId) {
|
val mangaInfo = infoCache.getOrElse(targetId) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
getMangaInfo(targetId)
|
getMangaInfo(targetId)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
@@ -72,9 +77,9 @@ abstract class Scrobbler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean {
|
suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean {
|
||||||
return runCatching {
|
return runCatchingCancellable {
|
||||||
scrobble(mangaId, chapter)
|
scrobble(mangaId, chapter)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
}.isSuccess
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class MangaSearchRepository @Inject constructor(
|
class MangaSearchRepository @Inject constructor(
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
@@ -33,7 +34,7 @@ class MangaSearchRepository @Inject constructor(
|
|||||||
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
||||||
settings.getMangaSources(includeHidden = false).asFlow()
|
settings.getMangaSources(includeHidden = false).asFlow()
|
||||||
.flatMapMerge(concurrency) { source ->
|
.flatMapMerge(concurrency) { source ->
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
mangaRepositoryFactory.create(source).getList(
|
mangaRepositoryFactory.create(source).getList(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
query = query,
|
query = query,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||||
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||||
|
import kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MangaListActivity :
|
class MangaListActivity :
|
||||||
@@ -41,6 +42,7 @@ class MangaListActivity :
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = if (source == MangaSource.LOCAL) {
|
val fragment = if (source == MangaSource.LOCAL) {
|
||||||
LocalListFragment.newInstance()
|
LocalListFragment.newInstance()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||||
import org.koitharu.kotatsu.utils.ext.showKeyboard
|
import org.koitharu.kotatsu.utils.ext.showKeyboard
|
||||||
|
import kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
|
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
|
||||||
@@ -61,6 +62,7 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
|
|||||||
}
|
}
|
||||||
title = query
|
title = query
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
replace(R.id.container, SearchFragment.newInstance(source, q))
|
replace(R.id.container, SearchFragment.newInstance(source, q))
|
||||||
}
|
}
|
||||||
binding.searchView.clearFocus()
|
binding.searchView.clearFocus()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -12,7 +13,13 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
@@ -41,12 +48,13 @@ class SearchViewModel @AssistedInject constructor(
|
|||||||
list == null -> listOf(LoadingState)
|
list == null -> listOf(LoadingState)
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
EmptyState(
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_search,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = R.string.text_search_holder_secondary,
|
textSecondary = R.string.text_search_holder_secondary,
|
||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val result = ArrayList<ListModel>(list.size + 1)
|
val result = ArrayList<ListModel>(list.size + 1)
|
||||||
list.toUi(result, mode)
|
list.toUi(result, mode)
|
||||||
@@ -94,6 +102,8 @@ class SearchViewModel @AssistedInject constructor(
|
|||||||
mangaList.value = mangaList.value?.plus(list) ?: list
|
mangaList.value = mangaList.value?.plus(list) ?: list
|
||||||
}
|
}
|
||||||
hasNextPage.value = list.isNotEmpty()
|
hasNextPage.value = list.isNotEmpty()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
listError.value = e
|
listError.value = e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.*
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
private const val MAX_PARALLELISM = 4
|
private const val MAX_PARALLELISM = 4
|
||||||
private const val MIN_HAS_MORE_ITEMS = 8
|
private const val MIN_HAS_MORE_ITEMS = 8
|
||||||
@@ -47,13 +48,14 @@ class MultiSearchViewModel @AssistedInject constructor(
|
|||||||
loading -> LoadingState
|
loading -> LoadingState
|
||||||
error != null -> error.toErrorState(canRetry = true)
|
error != null -> error.toErrorState(canRetry = true)
|
||||||
else -> EmptyState(
|
else -> EmptyState(
|
||||||
icon = R.drawable.ic_empty_search,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = R.string.text_search_holder_secondary,
|
textSecondary = R.string.text_search_holder_secondary,
|
||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
loading -> list + LoadingFooter
|
loading -> list + LoadingFooter
|
||||||
else -> list
|
else -> list
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,8 @@ class MultiSearchViewModel @AssistedInject constructor(
|
|||||||
loadingData.value = true
|
loadingData.value = true
|
||||||
query.postValue(q)
|
query.postValue(q)
|
||||||
searchImpl(q)
|
searchImpl(q)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
listError.value = e
|
listError.value = e
|
||||||
} finally {
|
} finally {
|
||||||
@@ -98,7 +102,7 @@ class MultiSearchViewModel @AssistedInject constructor(
|
|||||||
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
|
||||||
val deferredList = sources.map { source ->
|
val deferredList = sources.map { source ->
|
||||||
async(dispatcher) {
|
async(dispatcher) {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
|
val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
|
||||||
.toUi(ListMode.GRID)
|
.toUi(ListMode.GRID)
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.preference.Preference
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
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 javax.inject.Inject
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|||||||
import org.koitharu.kotatsu.utils.FileSize
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
|
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
|
||||||
@@ -82,18 +83,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
clearCache(preference, CacheDir.PAGES)
|
clearCache(preference, CacheDir.PAGES)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||||
clearCache(preference, CacheDir.THUMBS)
|
clearCache(preference, CacheDir.THUMBS)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_COOKIES_CLEAR -> {
|
AppSettings.KEY_COOKIES_CLEAR -> {
|
||||||
clearCookies()
|
clearCookies()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
||||||
clearSearchHistory(preference)
|
clearSearchHistory(preference)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||||
viewLifecycleScope.launch {
|
viewLifecycleScope.launch {
|
||||||
trackerRepo.clearLogs()
|
trackerRepo.clearLogs()
|
||||||
@@ -107,6 +112,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettings.KEY_SHIKIMORI -> {
|
AppSettings.KEY_SHIKIMORI -> {
|
||||||
if (!shikimoriRepository.isAuthorized) {
|
if (!shikimoriRepository.isAuthorized) {
|
||||||
launchShikimoriAuth()
|
launchShikimoriAuth()
|
||||||
@@ -115,6 +121,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
super.onPreferenceTreeClick(preference)
|
super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
else -> super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +134,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
storageManager.clearCache(cache)
|
storageManager.clearCache(cache)
|
||||||
val size = storageManager.computeCacheSize(cache)
|
val size = storageManager.computeCacheSize(cache)
|
||||||
preference.summary = FileSize.BYTES.format(ctx, size)
|
preference.summary = FileSize.BYTES.format(ctx, size)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
preference.summary = e.getDisplayMessage(ctx.resources)
|
preference.summary = e.getDisplayMessage(ctx.resources)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
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 javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -20,7 +19,14 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
|||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.serializableArgument
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SourceSettingsFragment : BasePreferenceFragment(0) {
|
class SourceSettingsFragment : BasePreferenceFragment(0) {
|
||||||
@@ -66,12 +72,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
|||||||
startActivity(SourceAuthActivity.newIntent(preference.context, source))
|
startActivity(SourceAuthActivity.newIntent(preference.context, source))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
else -> super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
|
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
|
||||||
runCatching {
|
runCatchingCancellable {
|
||||||
preference.summary = null
|
preference.summary = null
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
requireNotNull(repository?.getAuthProvider()?.getUsername())
|
requireNotNull(repository?.getAuthProvider()?.getUsername())
|
||||||
@@ -91,6 +98,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
|
|||||||
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
|
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
|
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
|
||||||
}
|
}
|
||||||
error.printStackTraceDebug()
|
error.printStackTraceDebug()
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.progress.Progress
|
import org.koitharu.kotatsu.utils.progress.Progress
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||||
@@ -91,6 +91,8 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
|||||||
}
|
}
|
||||||
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
|
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
|
||||||
dismiss()
|
dismiss()
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onError(e)
|
onError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
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.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
||||||
|
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
|
||||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
||||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
|
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
|
||||||
@@ -21,8 +21,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class OnboardDialogFragment :
|
class OnboardDialogFragment :
|
||||||
AlertDialogFragment<DialogOnboardBinding>(),
|
AlertDialogFragment<DialogOnboardBinding>(),
|
||||||
OnListItemClickListener<SourceLocale>,
|
DialogInterface.OnClickListener, SourceLocaleListener {
|
||||||
DialogInterface.OnClickListener {
|
|
||||||
|
|
||||||
private val viewModel by viewModels<OnboardViewModel>()
|
private val viewModel by viewModels<OnboardViewModel>()
|
||||||
private var isWelcome: Boolean = false
|
private var isWelcome: Boolean = false
|
||||||
@@ -63,8 +62,8 @@ class OnboardDialogFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: SourceLocale, view: View) {
|
override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) {
|
||||||
viewModel.setItemChecked(item.key, !item.isChecked)
|
viewModel.setItemChecked(item.key, isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(dialog: DialogInterface?, which: Int) {
|
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.settings.onboard
|
package org.koitharu.kotatsu.settings.onboard
|
||||||
|
|
||||||
import androidx.collection.ArraySet
|
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
@@ -15,6 +12,8 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|||||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
import org.koitharu.kotatsu.utils.ext.map
|
import org.koitharu.kotatsu.utils.ext.map
|
||||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class OnboardViewModel @Inject constructor(
|
class OnboardViewModel @Inject constructor(
|
||||||
@@ -23,9 +22,9 @@ class OnboardViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val allSources = settings.remoteMangaSources
|
private val allSources = settings.remoteMangaSources
|
||||||
|
|
||||||
private val locales = allSources.mapTo(ArraySet()) { it.locale }
|
private val locales = allSources.groupBy { it.locale }
|
||||||
|
|
||||||
private val selectedLocales = locales.toMutableSet()
|
private val selectedLocales = locales.keys.toMutableSet()
|
||||||
|
|
||||||
val list = MutableLiveData<List<SourceLocale>?>()
|
val list = MutableLiveData<List<SourceLocale>?>()
|
||||||
|
|
||||||
@@ -64,13 +63,14 @@ class OnboardViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun rebuildList() {
|
private fun rebuildList() {
|
||||||
list.value = locales.map { key ->
|
list.value = locales.map { (key, srcs) ->
|
||||||
val locale = if (key != null) {
|
val locale = if (key != null) {
|
||||||
Locale(key)
|
Locale(key)
|
||||||
} else null
|
} else null
|
||||||
SourceLocale(
|
SourceLocale(
|
||||||
key = key,
|
key = key,
|
||||||
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
|
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
|
||||||
|
summary = srcs.joinToString { it.title },
|
||||||
isChecked = key in selectedLocales,
|
isChecked = key in selectedLocales,
|
||||||
)
|
)
|
||||||
}.sortedWith(SourceLocaleComparator())
|
}.sortedWith(SourceLocaleComparator())
|
||||||
@@ -87,11 +87,12 @@ class OnboardViewModel @Inject constructor(
|
|||||||
a?.key == null -> 1
|
a?.key == null -> 1
|
||||||
b?.key == null -> -1
|
b?.key == null -> -1
|
||||||
else -> {
|
else -> {
|
||||||
val index = deviceLocales.indexOf(a.key)
|
val indexA = deviceLocales.indexOf(a.key)
|
||||||
if (index == -1) {
|
val indexB = deviceLocales.indexOf(b.key)
|
||||||
|
if (indexA == -1 && indexB == -1) {
|
||||||
compareValues(a.title, b.title)
|
compareValues(a.title, b.title)
|
||||||
} else {
|
} else {
|
||||||
-2 - index
|
-2 - (indexA - indexB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ package org.koitharu.kotatsu.settings.onboard.adapter
|
|||||||
|
|
||||||
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.databinding.ItemSourceLocaleBinding
|
import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding
|
||||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
|
||||||
fun sourceLocaleAD(
|
fun sourceLocaleAD(
|
||||||
clickListener: OnListItemClickListener<SourceLocale>
|
listener: SourceLocaleListener,
|
||||||
) = adapterDelegateViewBinding<SourceLocale, SourceLocale, ItemSourceLocaleBinding>(
|
) = adapterDelegateViewBinding<SourceLocale, SourceLocale, ItemSourceLocaleBinding>(
|
||||||
{ inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) }
|
{ inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||||
clickListener.onItemClick(item, it)
|
listener.onItemCheckedChanged(item, isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.root.text = item.title ?: getString(R.string.other)
|
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
|
||||||
binding.root.isChecked = item.isChecked
|
binding.textViewDescription.textAndVisible = item.summary
|
||||||
|
binding.switchToggle.isChecked = item.isChecked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.onboard.adapter
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
|
|
||||||
|
interface SourceLocaleListener {
|
||||||
|
|
||||||
|
fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean)
|
||||||
|
}
|
||||||
@@ -2,15 +2,14 @@ package org.koitharu.kotatsu.settings.onboard.adapter
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
|
||||||
|
|
||||||
class SourceLocalesAdapter(
|
class SourceLocalesAdapter(
|
||||||
clickListener: OnListItemClickListener<SourceLocale>,
|
listener: SourceLocaleListener,
|
||||||
) : AsyncListDifferDelegationAdapter<SourceLocale>(DiffCallback()) {
|
) : AsyncListDifferDelegationAdapter<SourceLocale>(DiffCallback()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
delegatesManager.addDelegate(sourceLocaleAD(clickListener))
|
delegatesManager.addDelegate(sourceLocaleAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<SourceLocale>() {
|
private class DiffCallback : DiffUtil.ItemCallback<SourceLocale>() {
|
||||||
@@ -25,4 +24,4 @@ class SourceLocalesAdapter(
|
|||||||
newItem: SourceLocale,
|
newItem: SourceLocale,
|
||||||
): Boolean = oldItem == newItem
|
): Boolean = oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.settings.onboard.model
|
package org.koitharu.kotatsu.settings.onboard.model
|
||||||
|
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
data class SourceLocale(
|
data class SourceLocale(
|
||||||
val key: String?,
|
val key: String?,
|
||||||
val title: String?,
|
val title: String?,
|
||||||
|
val summary: String?,
|
||||||
val isChecked: Boolean,
|
val isChecked: Boolean,
|
||||||
) : Comparable<SourceLocale> {
|
) : Comparable<SourceLocale> {
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ class ToolsFragment :
|
|||||||
binding.cardUpdate.root.isVisible = false
|
binding.cardUpdate.root.isVisible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
binding.cardUpdate.textPrimary.text = getString(R.string.app_update_available_s, version.name)
|
binding.cardUpdate.textSecondary.text = getString(R.string.new_version_s, version.name)
|
||||||
binding.cardUpdate.textSecondary.text = version.description
|
binding.cardUpdate.textChangelog.text = version.description
|
||||||
binding.cardUpdate.root.isVisible = true
|
binding.cardUpdate.root.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||||
|
|
||||||
class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityResultContract<Uri?, Uri?>() {
|
class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityResultContract<Uri?, Uri?>() {
|
||||||
|
|
||||||
@@ -30,6 +31,6 @@ class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
|
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
|
||||||
return intent?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
return intent?.getParcelableExtraCompat(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.domain
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ShelfRepository @Inject constructor(
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val db: MangaDatabase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
|
||||||
|
return flow {
|
||||||
|
emit(null)
|
||||||
|
emitAll(localMangaRepository.watchReadableDirs())
|
||||||
|
}.mapLatest {
|
||||||
|
localMangaRepository.getList(0, null, sortOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
||||||
|
return db.favouriteCategoriesDao.observeAll()
|
||||||
|
.flatMapLatest { categories ->
|
||||||
|
val cats = categories.filter { it.isVisibleInLibrary }
|
||||||
|
if (cats.isEmpty()) {
|
||||||
|
flowOf(emptyMap())
|
||||||
|
} else {
|
||||||
|
observeCategoriesContent(cats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteLocalManga(ids: Set<Long>) {
|
||||||
|
val list = localMangaRepository.getList(0, null, null)
|
||||||
|
.filter { x -> x.id in ids }
|
||||||
|
coroutineScope {
|
||||||
|
list.map { manga ->
|
||||||
|
async {
|
||||||
|
val original = localMangaRepository.getRemoteManga(manga)
|
||||||
|
if (localMangaRepository.delete(manga)) {
|
||||||
|
runCatchingCancellable {
|
||||||
|
historyRepository.deleteOrSwap(manga, original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeCategoriesContent(
|
||||||
|
categories: List<FavouriteCategoryEntity>,
|
||||||
|
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(
|
||||||
|
categories.map { cat ->
|
||||||
|
val category = cat.toFavouriteCategory()
|
||||||
|
db.favouritesDao.observeAll(category.id, category.order)
|
||||||
|
.map { category to it.map { x -> x.manga.toManga(x.tags.toMangaTags()) } }
|
||||||
|
},
|
||||||
|
) { array -> array.toMap() }
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.library.ui
|
package org.koitharu.kotatsu.shelf.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -11,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import coil.ImageLoader
|
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 javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
@@ -19,25 +21,29 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
|
import org.koitharu.kotatsu.databinding.FragmentShelfBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||||
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
|
|
||||||
import org.koitharu.kotatsu.library.ui.adapter.LibraryListEventListener
|
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LibraryFragment :
|
class ShelfFragment :
|
||||||
BaseFragment<FragmentLibraryBinding>(),
|
BaseFragment<FragmentShelfBinding>(),
|
||||||
RecyclerViewOwner,
|
RecyclerViewOwner,
|
||||||
LibraryListEventListener {
|
ShelfListEventListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
@@ -45,15 +51,15 @@ class LibraryFragment :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
private val viewModel by viewModels<LibraryViewModel>()
|
private val viewModel by viewModels<ShelfViewModel>()
|
||||||
private var adapter: LibraryAdapter? = null
|
private var adapter: ShelfAdapter? = null
|
||||||
private var selectionController: SectionedSelectionController<LibrarySectionModel>? = null
|
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
|
||||||
|
|
||||||
override val recyclerView: RecyclerView
|
override val recyclerView: RecyclerView
|
||||||
get() = binding.recyclerView
|
get() = binding.recyclerView
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentLibraryBinding {
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentShelfBinding {
|
||||||
return FragmentLibraryBinding.inflate(inflater, container, false)
|
return FragmentShelfBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -62,9 +68,9 @@ class LibraryFragment :
|
|||||||
selectionController = SectionedSelectionController(
|
selectionController = SectionedSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
owner = this,
|
owner = this,
|
||||||
callback = LibrarySelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
|
callback = ShelfSelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
|
||||||
)
|
)
|
||||||
adapter = LibraryAdapter(
|
adapter = ShelfAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
coil = coil,
|
coil = coil,
|
||||||
listener = this,
|
listener = this,
|
||||||
@@ -73,7 +79,7 @@ class LibraryFragment :
|
|||||||
)
|
)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
addMenuProvider(LibraryMenuProvider(view.context, childFragmentManager, viewModel))
|
addMenuProvider(ShelfMenuProvider(view.context, childFragmentManager, viewModel))
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
@@ -86,29 +92,38 @@ class LibraryFragment :
|
|||||||
selectionController = null
|
selectionController = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Manga, section: LibrarySectionModel, view: View) {
|
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
|
||||||
if (selectionController?.onItemClick(section, item.id) != true) {
|
if (selectionController?.onItemClick(section, item.id) != true) {
|
||||||
val intent = DetailsActivity.newIntent(view.context, item)
|
val intent = DetailsActivity.newIntent(view.context, item)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: Manga, section: LibrarySectionModel, view: View): Boolean {
|
override fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean {
|
||||||
return selectionController?.onItemLongClick(section, item.id) ?: false
|
return selectionController?.onItemLongClick(section, item.id) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSectionClick(section: LibrarySectionModel, view: View) {
|
override fun onSectionClick(section: ShelfSectionModel, view: View) {
|
||||||
selectionController?.clear()
|
selectionController?.clear()
|
||||||
val intent = when (section) {
|
val intent = when (section) {
|
||||||
is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context)
|
is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context)
|
||||||
is LibrarySectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
|
is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
|
||||||
|
is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context)
|
||||||
|
is ShelfSectionModel.Local -> MangaListActivity.newIntent(view.context, MangaSource.LOCAL)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) = Unit
|
||||||
|
|
||||||
override fun onEmptyActionClick() = Unit
|
override fun onEmptyActionClick() {
|
||||||
|
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
Settings.Panel.ACTION_INTERNET_CONNECTIVITY
|
||||||
|
} else {
|
||||||
|
Settings.ACTION_WIRELESS_SETTINGS
|
||||||
|
}
|
||||||
|
startActivity(Intent(action))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
binding.recyclerView.updatePadding(
|
binding.recyclerView.updatePadding(
|
||||||
@@ -143,6 +158,6 @@ class LibraryFragment :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = LibraryFragment()
|
fun newInstance() = ShelfFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui
|
package org.koitharu.kotatsu.shelf.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -12,19 +12,19 @@ import java.util.*
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
|
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
|
||||||
import org.koitharu.kotatsu.library.ui.config.categories.LibraryCategoriesConfigSheet
|
import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfCategoriesConfigSheet
|
||||||
import org.koitharu.kotatsu.library.ui.config.size.LibrarySizeBottomSheet
|
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
|
||||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||||
import org.koitharu.kotatsu.utils.ext.startOfDay
|
import org.koitharu.kotatsu.utils.ext.startOfDay
|
||||||
|
|
||||||
class LibraryMenuProvider(
|
class ShelfMenuProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val fragmentManager: FragmentManager,
|
private val fragmentManager: FragmentManager,
|
||||||
private val viewModel: LibraryViewModel,
|
private val viewModel: ShelfViewModel,
|
||||||
) : MenuProvider {
|
) : MenuProvider {
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.opt_library, menu)
|
menuInflater.inflate(R.menu.opt_shelf, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
@@ -34,7 +34,7 @@ class LibraryMenuProvider(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_grid_size -> {
|
R.id.action_grid_size -> {
|
||||||
LibrarySizeBottomSheet.show(fragmentManager)
|
ShelfSizeBottomSheet.show(fragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_import -> {
|
R.id.action_import -> {
|
||||||
@@ -42,7 +42,7 @@ class LibraryMenuProvider(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_categories -> {
|
R.id.action_categories -> {
|
||||||
LibraryCategoriesConfigSheet.show(fragmentManager)
|
ShelfCategoriesConfigSheet.show(fragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui
|
package org.koitharu.kotatsu.shelf.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -6,48 +6,51 @@ import android.view.MenuItem
|
|||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
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.AbstractSelectionItemDecoration
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.flattenTo
|
import org.koitharu.kotatsu.parsers.util.flattenTo
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||||
|
|
||||||
class LibrarySelectionCallback(
|
class ShelfSelectionCallback(
|
||||||
private val recyclerView: RecyclerView,
|
private val recyclerView: RecyclerView,
|
||||||
private val fragmentManager: FragmentManager,
|
private val fragmentManager: FragmentManager,
|
||||||
private val viewModel: LibraryViewModel,
|
private val viewModel: ShelfViewModel,
|
||||||
) : SectionedSelectionController.Callback<LibrarySectionModel> {
|
) : SectionedSelectionController.Callback<ShelfSectionModel> {
|
||||||
|
|
||||||
private val context: Context
|
private val context: Context
|
||||||
get() = recyclerView.context
|
get() = recyclerView.context
|
||||||
|
|
||||||
override fun onCreateActionMode(
|
override fun onCreateActionMode(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
menu: Menu,
|
menu: Menu,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.mode_library, menu)
|
mode.menuInflater.inflate(R.menu.mode_shelf, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(
|
override fun onPrepareActionMode(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
menu: Menu,
|
menu: Menu,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
menu.findItem(R.id.action_remove).isVisible =
|
val checkedIds = controller.peekCheckedIds().entries
|
||||||
controller.peekCheckedIds().count { (_, v) -> v.isNotEmpty() } == 1
|
val singleKey = checkedIds.singleOrNull { (_, ids) -> ids.isNotEmpty() }?.key
|
||||||
|
menu.findItem(R.id.action_remove)?.isVisible = singleKey != null && singleKey !is ShelfSectionModel.Updated
|
||||||
|
menu.findItem(R.id.action_save)?.isVisible = singleKey !is ShelfSectionModel.Local
|
||||||
return super.onPrepareActionMode(controller, mode, menu)
|
return super.onPrepareActionMode(controller, mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(
|
override fun onActionItemClicked(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -57,41 +60,50 @@ class LibrarySelectionCallback(
|
|||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_favourite -> {
|
R.id.action_favourite -> {
|
||||||
FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller))
|
FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller))
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_save -> {
|
R.id.action_save -> {
|
||||||
DownloadService.confirmAndStart(context, collectSelectedItems(controller))
|
DownloadService.confirmAndStart(context, collectSelectedItems(controller))
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_remove -> {
|
R.id.action_remove -> {
|
||||||
val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false
|
val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false
|
||||||
when (group) {
|
when (group) {
|
||||||
is LibrarySectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
||||||
is LibrarySectionModel.History -> viewModel.removeFromHistory(ids)
|
is ShelfSectionModel.History -> viewModel.removeFromHistory(ids)
|
||||||
|
is ShelfSectionModel.Updated -> return false
|
||||||
|
is ShelfSectionModel.Local -> {
|
||||||
|
showDeletionConfirm(ids, mode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectionChanged(controller: SectionedSelectionController<LibrarySectionModel>, count: Int) {
|
override fun onSelectionChanged(controller: SectionedSelectionController<ShelfSectionModel>, count: Int) {
|
||||||
recyclerView.invalidateNestedItemDecorations()
|
recyclerView.invalidateNestedItemDecorations()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateItemDecoration(
|
override fun onCreateItemDecoration(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
section: LibrarySectionModel,
|
section: ShelfSectionModel,
|
||||||
): AbstractSelectionItemDecoration = MangaSelectionDecoration(context)
|
): AbstractSelectionItemDecoration = MangaSelectionDecoration(context)
|
||||||
|
|
||||||
private fun collectSelectedItemsMap(
|
private fun collectSelectedItemsMap(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
): Map<LibrarySectionModel, Set<Manga>> {
|
): Map<ShelfSectionModel, Set<Manga>> {
|
||||||
val snapshot = controller.peekCheckedIds()
|
val snapshot = controller.peekCheckedIds()
|
||||||
if (snapshot.isEmpty()) {
|
if (snapshot.isEmpty()) {
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
@@ -100,7 +112,7 @@ class LibrarySelectionCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun collectSelectedItems(
|
private fun collectSelectedItems(
|
||||||
controller: SectionedSelectionController<LibrarySectionModel>,
|
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||||
): Set<Manga> {
|
): Set<Manga> {
|
||||||
val snapshot = controller.peekCheckedIds()
|
val snapshot = controller.peekCheckedIds()
|
||||||
if (snapshot.isEmpty()) {
|
if (snapshot.isEmpty()) {
|
||||||
@@ -108,4 +120,19 @@ class LibrarySelectionCallback(
|
|||||||
}
|
}
|
||||||
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MaterialAlertDialogBuilder(context ?: return)
|
||||||
|
.setTitle(R.string.delete_manga)
|
||||||
|
.setMessage(context.getString(R.string.text_delete_local_manga_batch))
|
||||||
|
.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
|
viewModel.deleteLocal(ids)
|
||||||
|
mode.finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,66 @@
|
|||||||
package org.koitharu.kotatsu.library.ui
|
package org.koitharu.kotatsu.shelf.ui
|
||||||
|
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.lifecycle.LiveData
|
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 java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
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.domain.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.library.domain.LibraryRepository
|
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
|
||||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val HISTORY_MAX_SEGMENTS = 2
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryViewModel @Inject constructor(
|
class ShelfViewModel @Inject constructor(
|
||||||
repository: LibraryRepository,
|
private val repository: ShelfRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val favouritesRepository: FavouritesRepository,
|
private val favouritesRepository: FavouritesRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
|
private val networkStateObserver: NetworkStateObserver,
|
||||||
) : BaseViewModel(), ListExtraProvider {
|
) : BaseViewModel(), ListExtraProvider {
|
||||||
|
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||||
|
|
||||||
val content: LiveData<List<ListModel>> = combine(
|
val content: LiveData<List<ListModel>> = combine(
|
||||||
|
networkStateObserver,
|
||||||
historyRepository.observeAllWithHistory(),
|
historyRepository.observeAllWithHistory(),
|
||||||
|
repository.observeLocalManga(SortOrder.UPDATED),
|
||||||
repository.observeFavourites(),
|
repository.observeFavourites(),
|
||||||
) { history, favourites ->
|
trackingRepository.observeUpdatedManga(),
|
||||||
mapList(history, favourites)
|
) { isConnected, history, local, favourites, updated ->
|
||||||
}.catch { e ->
|
mapList(history, favourites, updated, local, isConnected)
|
||||||
emit(listOf(e.toErrorState(canRetry = false)))
|
}.debounce(500)
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
.catch { e ->
|
||||||
|
emit(listOf(e.toErrorState(canRetry = false)))
|
||||||
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
override suspend fun getCounter(mangaId: Long): Int {
|
override suspend fun getCounter(mangaId: Long): Int {
|
||||||
return trackingRepository.getNewChaptersCount(mangaId)
|
return trackingRepository.getNewChaptersCount(mangaId)
|
||||||
@@ -84,6 +94,13 @@ class LibraryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteLocal(ids: Set<Long>) {
|
||||||
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
|
repository.deleteLocalManga(ids)
|
||||||
|
onActionDone.postCall(ReversibleAction(R.string.removal_completed, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearHistory(minDate: Long) {
|
fun clearHistory(minDate: Long) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val stringRes = if (minDate <= 0) {
|
val stringRes = if (minDate <= 0) {
|
||||||
@@ -101,7 +118,7 @@ class LibraryViewModel @Inject constructor(
|
|||||||
val snapshot = content.value ?: return emptySet()
|
val snapshot = content.value ?: return emptySet()
|
||||||
val result = ArraySet<Manga>(ids.size)
|
val result = ArraySet<Manga>(ids.size)
|
||||||
for (section in snapshot) {
|
for (section in snapshot) {
|
||||||
if (section !is LibrarySectionModel) {
|
if (section !is ShelfSectionModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (item in section.items) {
|
for (item in section.items) {
|
||||||
@@ -119,13 +136,38 @@ class LibraryViewModel @Inject constructor(
|
|||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
history: List<MangaWithHistory>,
|
history: List<MangaWithHistory>,
|
||||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
favourites: Map<FavouriteCategory, List<Manga>>,
|
||||||
|
updated: Map<Manga, Int>,
|
||||||
|
local: List<Manga>,
|
||||||
|
isNetworkAvailable: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val result = ArrayList<ListModel>(favourites.keys.size + 1)
|
val result = ArrayList<ListModel>(favourites.keys.size + 3)
|
||||||
if (history.isNotEmpty()) {
|
if (isNetworkAvailable) {
|
||||||
mapHistory(result, history)
|
if (history.isNotEmpty()) {
|
||||||
}
|
mapHistory(result, history)
|
||||||
if (favourites.isNotEmpty()) {
|
}
|
||||||
mapFavourites(result, favourites)
|
if (local.isNotEmpty()) {
|
||||||
|
mapLocal(result, local)
|
||||||
|
}
|
||||||
|
if (updated.isNotEmpty()) {
|
||||||
|
mapUpdated(result, updated)
|
||||||
|
}
|
||||||
|
if (favourites.isNotEmpty()) {
|
||||||
|
mapFavourites(result, favourites)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += EmptyHint(
|
||||||
|
icon = R.drawable.ic_empty_common,
|
||||||
|
textPrimary = R.string.network_unavailable,
|
||||||
|
textSecondary = R.string.network_unavailable_hint,
|
||||||
|
actionStringRes = R.string.manage,
|
||||||
|
)
|
||||||
|
val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL }
|
||||||
|
if (offlineHistory.isNotEmpty()) {
|
||||||
|
mapHistory(result, offlineHistory)
|
||||||
|
}
|
||||||
|
if (local.isNotEmpty()) {
|
||||||
|
mapLocal(result, local)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
result += EmptyState(
|
result += EmptyState(
|
||||||
@@ -134,42 +176,61 @@ class LibraryViewModel @Inject constructor(
|
|||||||
textSecondary = R.string.text_shelf_holder_secondary,
|
textSecondary = R.string.text_shelf_holder_secondary,
|
||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
val one = result.singleOrNull()
|
||||||
|
if (one is EmptyHint) {
|
||||||
|
result[0] = one.toState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.trimToSize()
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun mapHistory(
|
private suspend fun mapHistory(
|
||||||
destination: MutableList<in LibrarySectionModel.History>,
|
destination: MutableList<in ShelfSectionModel.History>,
|
||||||
list: List<MangaWithHistory>,
|
list: List<MangaWithHistory>,
|
||||||
) {
|
) {
|
||||||
val showPercent = settings.isReadingIndicatorsEnabled
|
val showPercent = settings.isReadingIndicatorsEnabled
|
||||||
val groups = list.groupByTo(LinkedHashMap()) { timeAgo(it.history.updatedAt) }
|
destination += ShelfSectionModel.History(
|
||||||
while (groups.size > HISTORY_MAX_SEGMENTS) {
|
items = list.map { (manga, history) ->
|
||||||
val lastKey = groups.keys.last()
|
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
||||||
val subList = groups.remove(lastKey) ?: continue
|
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
||||||
groups[groups.keys.last()]?.addAll(subList)
|
manga.toGridModel(counter, percent)
|
||||||
}
|
},
|
||||||
for ((timeAgo, subList) in groups) {
|
showAllButtonText = R.string.show_all,
|
||||||
destination += LibrarySectionModel.History(
|
)
|
||||||
items = subList.map { (manga, history) ->
|
}
|
||||||
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
|
||||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
private suspend fun mapUpdated(
|
||||||
manga.toGridModel(counter, percent)
|
destination: MutableList<in ShelfSectionModel.Updated>,
|
||||||
},
|
updated: Map<Manga, Int>,
|
||||||
timeAgo = timeAgo,
|
) {
|
||||||
showAllButtonText = R.string.show_all,
|
val showPercent = settings.isReadingIndicatorsEnabled
|
||||||
)
|
destination += ShelfSectionModel.Updated(
|
||||||
}
|
items = updated.map { (manga, counter) ->
|
||||||
|
val percent = if (showPercent) getProgress(manga.id) else PROGRESS_NONE
|
||||||
|
manga.toGridModel(counter, percent)
|
||||||
|
},
|
||||||
|
showAllButtonText = R.string.show_all,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapLocal(
|
||||||
|
destination: MutableList<in ShelfSectionModel.Local>,
|
||||||
|
local: List<Manga>,
|
||||||
|
) {
|
||||||
|
destination += ShelfSectionModel.Local(
|
||||||
|
items = local.toUi(ListMode.GRID, this),
|
||||||
|
showAllButtonText = R.string.show_all,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun mapFavourites(
|
private suspend fun mapFavourites(
|
||||||
destination: MutableList<in LibrarySectionModel.Favourites>,
|
destination: MutableList<in ShelfSectionModel.Favourites>,
|
||||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
favourites: Map<FavouriteCategory, List<Manga>>,
|
||||||
) {
|
) {
|
||||||
for ((category, list) in favourites) {
|
for ((category, list) in favourites) {
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
destination += LibrarySectionModel.Favourites(
|
destination += ShelfSectionModel.Favourites(
|
||||||
items = list.toUi(ListMode.GRID, this),
|
items = list.toUi(ListMode.GRID, this),
|
||||||
category = category,
|
category = category,
|
||||||
showAllButtonText = R.string.show_all,
|
showAllButtonText = R.string.show_all,
|
||||||
@@ -177,14 +238,4 @@ class LibraryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun timeAgo(date: Date): DateTimeAgo {
|
|
||||||
val diffDays = -date.daysDiff(System.currentTimeMillis())
|
|
||||||
return when {
|
|
||||||
diffDays < 1 -> DateTimeAgo.Today
|
|
||||||
diffDays == 1 -> DateTimeAgo.Yesterday
|
|
||||||
diffDays <= 3 -> DateTimeAgo.DaysAgo(diffDays)
|
|
||||||
else -> DateTimeAgo.LongAgo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.adapter
|
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||||
@@ -27,4 +27,4 @@ class MangaItemDiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|||||||
else -> super.getChangePayload(oldItem, newItem)
|
else -> super.getChangePayload(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.adapter
|
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.adapter
|
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -6,30 +6,31 @@ 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 kotlin.jvm.internal.Intrinsics
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
||||||
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.shelf.ui.model.ShelfSectionModel
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
class LibraryAdapter(
|
class ShelfAdapter(
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
listener: LibraryListEventListener,
|
listener: ShelfListEventListener,
|
||||||
sizeResolver: ItemSizeResolver,
|
sizeResolver: ItemSizeResolver,
|
||||||
selectionController: SectionedSelectionController<LibrarySectionModel>,
|
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val pool = RecyclerView.RecycledViewPool()
|
val pool = RecyclerView.RecycledViewPool()
|
||||||
delegatesManager
|
delegatesManager
|
||||||
.addDelegate(
|
.addDelegate(
|
||||||
libraryGroupAD(
|
shelfGroupAD(
|
||||||
sharedPool = pool,
|
sharedPool = pool,
|
||||||
lifecycleOwner = lifecycleOwner,
|
lifecycleOwner = lifecycleOwner,
|
||||||
coil = coil,
|
coil = coil,
|
||||||
@@ -40,12 +41,13 @@ class LibraryAdapter(
|
|||||||
)
|
)
|
||||||
.addDelegate(loadingStateAD())
|
.addDelegate(loadingStateAD())
|
||||||
.addDelegate(loadingFooterAD())
|
.addDelegate(loadingFooterAD())
|
||||||
|
.addDelegate(emptyHintAD(listener))
|
||||||
.addDelegate(emptyStateListAD(coil, listener))
|
.addDelegate(emptyStateListAD(coil, listener))
|
||||||
.addDelegate(errorStateListAD(listener))
|
.addDelegate(errorStateListAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
override fun getSectionText(context: Context, position: Int): CharSequence {
|
||||||
val item = items.getOrNull(position) as? LibrarySectionModel
|
val item = items.getOrNull(position) as? ShelfSectionModel
|
||||||
return item?.getTitle(context.resources) ?: ""
|
return item?.getTitle(context.resources) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +55,10 @@ class LibraryAdapter(
|
|||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> {
|
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
|
||||||
oldItem.key == newItem.key
|
oldItem.key == newItem.key
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +69,7 @@ class LibraryAdapter(
|
|||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
||||||
return when {
|
return when {
|
||||||
oldItem is LibrarySectionModel && newItem is LibrarySectionModel -> Unit
|
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> Unit
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
else -> super.getChangePayload(oldItem, newItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.adapter
|
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||||
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
@@ -20,14 +20,14 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
|
import org.koitharu.kotatsu.utils.ext.removeItemDecoration
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
fun libraryGroupAD(
|
fun shelfGroupAD(
|
||||||
sharedPool: RecyclerView.RecycledViewPool,
|
sharedPool: RecyclerView.RecycledViewPool,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
sizeResolver: ItemSizeResolver,
|
sizeResolver: ItemSizeResolver,
|
||||||
selectionController: SectionedSelectionController<LibrarySectionModel>,
|
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||||
listener: LibraryListEventListener,
|
listener: ShelfListEventListener,
|
||||||
) = adapterDelegateViewBinding<LibrarySectionModel, ListModel, ItemListGroupBinding>(
|
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
|
||||||
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
val listenerAdapter = object : OnListItemClickListener<Manga>, View.OnClickListener {
|
val listenerAdapter = object : OnListItemClickListener<Manga>, View.OnClickListener {
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
interface ShelfListEventListener : ListStateHolderListener {
|
||||||
|
|
||||||
|
fun onItemClick(item: Manga, section: ShelfSectionModel, view: View)
|
||||||
|
|
||||||
|
fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean
|
||||||
|
|
||||||
|
fun onSectionClick(section: ShelfSectionModel, view: View)
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.config.categories
|
package org.koitharu.kotatsu.shelf.ui.config.categories
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
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.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
|
||||||
class LibraryCategoriesConfigAdapter(
|
class ShelfCategoriesConfigAdapter(
|
||||||
listener: OnListItemClickListener<FavouriteCategory>,
|
listener: OnListItemClickListener<FavouriteCategory>,
|
||||||
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
|
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
delegatesManager.addDelegate(libraryCategoryAD(listener))
|
delegatesManager.addDelegate(shelfCategoryAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
|
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.config.categories
|
package org.koitharu.kotatsu.shelf.ui.config.categories
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -15,12 +15,12 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|||||||
import org.koitharu.kotatsu.databinding.SheetBaseBinding
|
import org.koitharu.kotatsu.databinding.SheetBaseBinding
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LibraryCategoriesConfigSheet :
|
class ShelfCategoriesConfigSheet :
|
||||||
BaseBottomSheet<SheetBaseBinding>(),
|
BaseBottomSheet<SheetBaseBinding>(),
|
||||||
OnListItemClickListener<FavouriteCategory>,
|
OnListItemClickListener<FavouriteCategory>,
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
private val viewModel by viewModels<LibraryCategoriesConfigViewModel>()
|
private val viewModel by viewModels<ShelfCategoriesConfigViewModel>()
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
|
||||||
return SheetBaseBinding.inflate(inflater, container, false)
|
return SheetBaseBinding.inflate(inflater, container, false)
|
||||||
@@ -31,7 +31,7 @@ class LibraryCategoriesConfigSheet :
|
|||||||
binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
|
binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
|
||||||
binding.buttonDone.isVisible = true
|
binding.buttonDone.isVisible = true
|
||||||
binding.buttonDone.setOnClickListener(this)
|
binding.buttonDone.setOnClickListener(this)
|
||||||
val adapter = LibraryCategoriesConfigAdapter(this)
|
val adapter = ShelfCategoriesConfigAdapter(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
||||||
@@ -47,8 +47,8 @@ class LibraryCategoriesConfigSheet :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "LibraryCategoriesConfigSheet"
|
private const val TAG = "ShelfCategoriesConfigSheet"
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = LibraryCategoriesConfigSheet().show(fm, TAG)
|
fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.config.categories
|
package org.koitharu.kotatsu.shelf.ui.config.categories
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
|||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryCategoriesConfigViewModel @Inject constructor(
|
class ShelfCategoriesConfigViewModel @Inject constructor(
|
||||||
private val favouritesRepository: FavouritesRepository,
|
private val favouritesRepository: FavouritesRepository,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.config.categories
|
package org.koitharu.kotatsu.shelf.ui.config.categories
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
|
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
|
||||||
|
|
||||||
fun libraryCategoryAD(
|
fun shelfCategoryAD(
|
||||||
listener: OnListItemClickListener<FavouriteCategory>,
|
listener: OnListItemClickListener<FavouriteCategory>,
|
||||||
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
|
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
|
||||||
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.library.ui.config.size
|
package org.koitharu.kotatsu.shelf.ui.config.size
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -8,17 +8,17 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import com.google.android.material.slider.LabelFormatter
|
import com.google.android.material.slider.LabelFormatter
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.databinding.SheetLibrarySizeBinding
|
import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LibrarySizeBottomSheet :
|
class ShelfSizeBottomSheet :
|
||||||
BaseBottomSheet<SheetLibrarySizeBinding>(),
|
BaseBottomSheet<SheetShelfSizeBinding>(),
|
||||||
Slider.OnChangeListener,
|
Slider.OnChangeListener,
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ class LibrarySizeBottomSheet :
|
|||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
private var labelFormatter: LabelFormatter? = null
|
private var labelFormatter: LabelFormatter? = null
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetLibrarySizeBinding {
|
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetShelfSizeBinding {
|
||||||
return SheetLibrarySizeBinding.inflate(inflater, container, false)
|
return SheetShelfSizeBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -51,16 +51,17 @@ class LibrarySizeBottomSheet :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
|
val slider = binding.sliderGrid
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.button_small -> binding.sliderGrid.value -= binding.sliderGrid.stepSize
|
R.id.button_small -> slider.setValueRounded(slider.value - slider.stepSize)
|
||||||
R.id.button_large -> binding.sliderGrid.value += binding.sliderGrid.stepSize
|
R.id.button_large -> slider.setValueRounded(slider.value + slider.stepSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "LibrarySizeBottomSheet"
|
private const val TAG = "ShelfSizeBottomSheet"
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = LibrarySizeBottomSheet().show(fm, TAG)
|
fun show(fm: FragmentManager) = ShelfSizeBottomSheet().show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.model
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
|
|
||||||
|
sealed interface ShelfSectionModel : ListModel {
|
||||||
|
|
||||||
|
val items: List<MangaItemModel>
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val showAllButtonText: Int
|
||||||
|
|
||||||
|
val key: String
|
||||||
|
fun getTitle(resources: Resources): CharSequence
|
||||||
|
|
||||||
|
override fun toString(): String
|
||||||
|
|
||||||
|
class History(
|
||||||
|
override val items: List<MangaItemModel>,
|
||||||
|
override val showAllButtonText: Int,
|
||||||
|
) : ShelfSectionModel {
|
||||||
|
|
||||||
|
override val key = "history"
|
||||||
|
|
||||||
|
override fun getTitle(resources: Resources) = resources.getString(R.string.history)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as History
|
||||||
|
|
||||||
|
if (showAllButtonText != other.showAllButtonText) return false
|
||||||
|
if (items != other.items) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = items.hashCode()
|
||||||
|
result = 31 * result + showAllButtonText.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = key
|
||||||
|
}
|
||||||
|
|
||||||
|
class Favourites(
|
||||||
|
override val items: List<MangaItemModel>,
|
||||||
|
val category: FavouriteCategory,
|
||||||
|
override val showAllButtonText: Int,
|
||||||
|
) : ShelfSectionModel {
|
||||||
|
|
||||||
|
override val key = "fav_${category.id}"
|
||||||
|
|
||||||
|
override fun getTitle(resources: Resources) = category.title
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Favourites
|
||||||
|
|
||||||
|
if (category != other.category) return false
|
||||||
|
if (showAllButtonText != other.showAllButtonText) return false
|
||||||
|
if (items != other.items) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = items.hashCode()
|
||||||
|
result = 31 * result + category.hashCode()
|
||||||
|
result = 31 * result + showAllButtonText.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = key
|
||||||
|
}
|
||||||
|
|
||||||
|
class Updated(
|
||||||
|
override val items: List<MangaItemModel>,
|
||||||
|
override val showAllButtonText: Int,
|
||||||
|
) : ShelfSectionModel {
|
||||||
|
|
||||||
|
override val key = "upd"
|
||||||
|
|
||||||
|
override fun getTitle(resources: Resources) = resources.getString(R.string.updated)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Updated
|
||||||
|
|
||||||
|
if (items != other.items) return false
|
||||||
|
if (showAllButtonText != other.showAllButtonText) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = items.hashCode()
|
||||||
|
result = 31 * result + showAllButtonText
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = key
|
||||||
|
}
|
||||||
|
|
||||||
|
class Local(
|
||||||
|
override val items: List<MangaItemModel>,
|
||||||
|
override val showAllButtonText: Int,
|
||||||
|
) : ShelfSectionModel {
|
||||||
|
|
||||||
|
override val key = "local"
|
||||||
|
|
||||||
|
override fun getTitle(resources: Resources) = resources.getString(R.string.local_storage)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Local
|
||||||
|
|
||||||
|
if (items != other.items) return false
|
||||||
|
if (showAllButtonText != other.showAllButtonText) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = items.hashCode()
|
||||||
|
result = 31 * result + showAllButtonText
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = key
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.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 kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SuggestionsActivity :
|
class SuggestionsActivity :
|
||||||
@@ -28,6 +29,7 @@ class SuggestionsActivity :
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = SuggestionsFragment.newInstance()
|
val fragment = SuggestionsFragment.newInstance()
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user