Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3eab1a2a0 | |||
| 79d9dc7b24 | |||
| 7b573f8e6b | |||
|
|
7bd769e294 | ||
|
|
fde5f86313 | ||
| 3c23bf7ec9 | |||
| 4665f8b74e | |||
| 5a43e677c5 | |||
| 38d4274ece | |||
| 0e5221fa6e | |||
| b458bde8a1 | |||
| c663d10515 | |||
| cec19c3db3 | |||
|
|
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 | |||
| af2adeba13 | |||
| 93c6bec452 | |||
| c944044465 | |||
| 8a63ca2310 | |||
| 12e5e3b35e | |||
| 553a85ef86 | |||
| de7012cabf |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
|||||||
|
ko_fi: xtimms
|
||||||
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>
|
|
||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -4,6 +4,6 @@
|
|||||||
<option name="jvmTarget" value="1.8" />
|
<option name="jvmTarget" value="1.8" />
|
||||||
</component>
|
</component>
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.7.10" />
|
<option name="version" value="1.7.20" />
|
||||||
</component>
|
</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 499
|
versionCode 503
|
||||||
versionName '4.0-rc1'
|
versionName '4.0.3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -83,15 +83,15 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:b1990c7918') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
|
||||||
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.6.0'
|
implementation 'androidx.activity:activity-ktx:1.6.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
implementation 'androidx.fragment:fragment-ktx:1.5.4'
|
||||||
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'
|
||||||
@@ -103,7 +103,7 @@ dependencies {
|
|||||||
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-alpha05'
|
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'
|
||||||
|
|
||||||
@@ -123,9 +123,9 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.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:0ff0278f0f'
|
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'
|
||||||
@@ -134,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'
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -105,8 +108,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/manga_shelf"
|
android:label="@string/manga_shelf">
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -124,18 +126,18 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
||||||
android:label="@string/downloads"
|
android:label="@string/downloads"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop" />
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
|
||||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
||||||
<activity
|
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
|
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
||||||
android:label="@string/sync" />
|
android:label="@string/sync" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
||||||
android:label="@string/color_correction" />
|
android:label="@string/color_correction" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
|
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
|
||||||
val isAmoled = settings.isAmoledTheme
|
val isAmoled = settings.isAmoledTheme
|
||||||
val isDynamic = settings.isDynamicTheme
|
val isDynamic = settings.isDynamicTheme
|
||||||
// TODO support DialogWhenLarge theme
|
|
||||||
when {
|
when {
|
||||||
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
|
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
|
||||||
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
|
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import android.view.View.OnLongClickListener
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||||
|
|
||||||
class AdapterDelegateClickListenerAdapter<I>(
|
class AdapterDelegateClickListenerAdapter<I>(
|
||||||
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<I, *>,
|
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<out I, *>,
|
||||||
private val clickListener: OnListItemClickListener<I>,
|
private val clickListener: OnListItemClickListener<I>,
|
||||||
) : OnClickListener, OnLongClickListener {
|
) : OnClickListener, OnLongClickListener {
|
||||||
|
|
||||||
@@ -17,4 +17,4 @@ class AdapterDelegateClickListenerAdapter<I>(
|
|||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
return clickListener.onItemLongClick(adapterDelegate.item, v)
|
return clickListener.onItemLongClick(adapterDelegate.item, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
|
|||||||
fun TagEntity.toMangaTag() = MangaTag(
|
fun TagEntity.toMangaTag() = MangaTag(
|
||||||
key = this.key,
|
key = this.key,
|
||||||
title = this.title.toTitleCase(),
|
title = this.title.toTitleCase(),
|
||||||
source = MangaSource(this.source) ?: MangaSource.DUMMY,
|
source = MangaSource(this.source),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
||||||
@@ -28,7 +28,7 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
|||||||
coverUrl = this.coverUrl,
|
coverUrl = this.coverUrl,
|
||||||
largeCoverUrl = this.largeCoverUrl,
|
largeCoverUrl = this.largeCoverUrl,
|
||||||
author = this.author,
|
author = this.author,
|
||||||
source = MangaSource(this.source) ?: MangaSource.DUMMY,
|
source = MangaSource(this.source),
|
||||||
tags = tags,
|
tags = tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
fun MangaSource.getLocaleTitle(): String? {
|
fun MangaSource.getLocaleTitle(): String? {
|
||||||
val lc = Locale(locale ?: return null)
|
val lc = Locale(locale ?: return null)
|
||||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
fun MangaSource(name: String): MangaSource {
|
||||||
fun MangaSource(name: String): MangaSource? {
|
|
||||||
MangaSource.values().forEach {
|
MangaSource.values().forEach {
|
||||||
if (it.name == name) return it
|
if (it.name == name) return it
|
||||||
}
|
}
|
||||||
return null
|
return MangaSource.DUMMY
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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.onSuccess
|
||||||
|
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 val value: Boolean
|
||||||
|
get() = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class FlowNetworkCallback(
|
||||||
|
private val producerScope: ProducerScope<Boolean>,
|
||||||
|
) : NetworkCallback() {
|
||||||
|
|
||||||
|
private var prevValue = value
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) = update()
|
||||||
|
|
||||||
|
override fun onLost(network: Network) = update()
|
||||||
|
|
||||||
|
override fun onUnavailable() = update()
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
val newValue = connectivityManager.isNetworkAvailable
|
||||||
|
if (newValue != prevValue) {
|
||||||
|
producerScope.trySendBlocking(newValue).onSuccess {
|
||||||
|
prevValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import coil.network.HttpException
|
|||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -27,6 +26,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
|||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
private const val FALLBACK_SIZE = 9999 // largest icon
|
private const val FALLBACK_SIZE = 9999 // largest icon
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ class FaviconFetcher(
|
|||||||
|
|
||||||
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||||
return if (data.scheme == URI_SCHEME_FAVICON) {
|
return if (data.scheme == URI_SCHEME_FAVICON) {
|
||||||
val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null
|
val mangaSource = MangaSource(data.schemeSpecificPart)
|
||||||
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
|
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
enum class AppSection {
|
|
||||||
|
|
||||||
LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.model.ZoomMode
|
|||||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
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
|
||||||
@@ -44,14 +45,26 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val remoteMangaSources: Set<MangaSource>
|
val remoteMangaSources: Set<MangaSource>
|
||||||
get() = Collections.unmodifiableSet(remoteSources)
|
get() = Collections.unmodifiableSet(remoteSources)
|
||||||
|
|
||||||
|
var shelfSections: List<ShelfSection>
|
||||||
|
get() {
|
||||||
|
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
|
||||||
|
val values = enumValues<ShelfSection>()
|
||||||
|
if (raw.isNullOrEmpty()) {
|
||||||
|
return values.toList()
|
||||||
|
}
|
||||||
|
return raw.split('|')
|
||||||
|
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
|
||||||
|
.distinct()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
val raw = value.joinToString("|") { it.ordinal.toString() }
|
||||||
|
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
|
||||||
|
}
|
||||||
|
|
||||||
var listMode: ListMode
|
var listMode: ListMode
|
||||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
||||||
|
|
||||||
var defaultSection: AppSection
|
|
||||||
get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY)
|
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) }
|
|
||||||
|
|
||||||
val theme: Int
|
val theme: Int
|
||||||
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
|
||||||
@@ -218,6 +231,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
|
||||||
@@ -337,6 +353,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
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"
|
||||||
|
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
|
||||||
|
|
||||||
// About
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -15,15 +15,23 @@ 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.inputmethod.EditorInfoCompat
|
||||||
|
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.lifecycle.whenResumed
|
||||||
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 +42,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.shelf.ui.ShelfFragment
|
|
||||||
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,11 +55,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 kotlin.text.Typography.dagger
|
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"
|
||||||
|
|
||||||
@@ -116,6 +130,8 @@ 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)
|
||||||
|
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
@@ -139,11 +155,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
||||||
if (fragment is ShelfFragment) {
|
adjustFabVisibility(topFragment = fragment)
|
||||||
binding.fab?.show()
|
|
||||||
} else {
|
|
||||||
binding.fab?.hide()
|
|
||||||
}
|
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
binding.appbar.setExpanded(true)
|
binding.appbar.setExpanded(true)
|
||||||
}
|
}
|
||||||
@@ -261,6 +273,20 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onFeedAvailabilityChanged(isFeedAvailable: Boolean) {
|
||||||
|
navigationDelegate.setItemVisibility(R.id.nav_feed, isFeedAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onIncognitoModeChanged(isIncognito: Boolean) {
|
||||||
|
var options = binding.searchView.imeOptions
|
||||||
|
options = if (isIncognito) {
|
||||||
|
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||||
|
} else {
|
||||||
|
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
||||||
|
}
|
||||||
|
binding.searchView.imeOptions = options
|
||||||
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
binding.fab?.isEnabled = !isLoading
|
binding.fab?.isEnabled = !isLoading
|
||||||
}
|
}
|
||||||
@@ -296,8 +322,13 @@ class MainActivity :
|
|||||||
private fun onFirstStart() {
|
private fun onFirstStart() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
|
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
|
||||||
when {
|
when {
|
||||||
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
!settings.isSourcesSelected -> whenResumed {
|
||||||
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.newSources.isNotEmpty() -> whenResumed {
|
||||||
|
NewSourcesDialogFragment.show(supportFragmentManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
TrackWorker.setup(applicationContext)
|
TrackWorker.setup(applicationContext)
|
||||||
@@ -312,18 +343,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 ShelfFragment
|
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.shelf.ui.ShelfFragment
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -96,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()
|
||||||
|
|||||||
@@ -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>>()
|
||||||
@@ -56,7 +66,9 @@ class PageLoader @Inject constructor(
|
|||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
loaderScope.cancel()
|
loaderScope.cancel()
|
||||||
tasks.clear()
|
synchronized(tasks) {
|
||||||
|
tasks.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPrefetchApplicable(): Boolean {
|
fun isPrefetchApplicable(): Boolean {
|
||||||
@@ -93,7 +105,9 @@ class PageLoader @Inject constructor(
|
|||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
task = loadPageAsyncImpl(page)
|
task = loadPageAsyncImpl(page)
|
||||||
tasks[page.id] = task
|
synchronized(tasks) {
|
||||||
|
tasks[page.id] = task
|
||||||
|
}
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +139,9 @@ class PageLoader @Inject constructor(
|
|||||||
while (prefetchQueue.isNotEmpty()) {
|
while (prefetchQueue.isNotEmpty()) {
|
||||||
val page = prefetchQueue.pollFirst() ?: return
|
val page = prefetchQueue.pollFirst() ?: return
|
||||||
if (cache[page.url] == null) {
|
if (cache[page.url] == null) {
|
||||||
tasks[page.id] = loadPageAsyncImpl(page)
|
synchronized(tasks) {
|
||||||
|
tasks[page.id] = loadPageAsyncImpl(page)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,4 +213,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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import org.koitharu.kotatsu.utils.ext.isReportable
|
|||||||
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
|
import org.koitharu.kotatsu.utils.ext.observeWithPrevious
|
||||||
import org.koitharu.kotatsu.utils.ext.postDelayed
|
import org.koitharu.kotatsu.utils.ext.postDelayed
|
||||||
import org.koitharu.kotatsu.utils.ext.report
|
import org.koitharu.kotatsu.utils.ext.report
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -388,7 +389,7 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
if (uiState.isSliderAvailable()) {
|
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
|
||||||
|
|||||||
@@ -106,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,
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,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,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -15,7 +16,6 @@ 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 {
|
||||||
@@ -32,6 +32,7 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
|
|||||||
}
|
}
|
||||||
val query = intent.getStringExtra(EXTRA_QUERY)
|
val query = intent.getStringExtra(EXTRA_QUERY)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
|
||||||
with(binding.searchView) {
|
with(binding.searchView) {
|
||||||
queryHint = getString(R.string.search_on_s, source.title)
|
queryHint = getString(R.string.search_on_s, source.title)
|
||||||
setOnQueryTextListener(this@SearchActivity)
|
setOnQueryTextListener(this@SearchActivity)
|
||||||
@@ -72,6 +73,16 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
|
|||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean = false
|
override fun onQueryTextChange(newText: String?): Boolean = false
|
||||||
|
|
||||||
|
private fun onIncognitoModeChanged(isIncognito: Boolean) {
|
||||||
|
var options = binding.searchView.imeOptions
|
||||||
|
options = if (isIncognito) {
|
||||||
|
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||||
|
} else {
|
||||||
|
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
|
||||||
|
}
|
||||||
|
binding.searchView.imeOptions = options
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
private const val EXTRA_SOURCE = "source"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ 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,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ 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,
|
||||||
|
|||||||
@@ -3,17 +3,28 @@ package org.koitharu.kotatsu.search.ui.suggestion
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
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.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||||
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.search.ui.suggestion.model.SearchSuggestionItem
|
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val DEBOUNCE_TIMEOUT = 500L
|
private const val DEBOUNCE_TIMEOUT = 500L
|
||||||
private const val MAX_MANGA_ITEMS = 6
|
private const val MAX_MANGA_ITEMS = 6
|
||||||
@@ -30,6 +41,12 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
private val query = MutableStateFlow("")
|
private val query = MutableStateFlow("")
|
||||||
private var suggestionJob: Job? = null
|
private var suggestionJob: Job? = null
|
||||||
|
|
||||||
|
val isIncognitoModeEnabled = settings.observeAsLiveData(
|
||||||
|
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||||
|
valueProducer = { isIncognitoModeEnabled },
|
||||||
|
)
|
||||||
|
|
||||||
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
|
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -41,7 +58,11 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveQuery(query: String) {
|
fun saveQuery(query: String) {
|
||||||
repository.saveSearchQuery(query)
|
launchJob(Dispatchers.Default) {
|
||||||
|
if (!settings.isIncognitoModeEnabled) {
|
||||||
|
repository.saveSearchQuery(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSearchHistory() {
|
fun clearSearchHistory() {
|
||||||
|
|||||||
@@ -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,15 +22,15 @@ 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>?>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (settings.isSourcesSelected) {
|
if (settings.isSourcesSelected) {
|
||||||
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x)?.locale })
|
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x).locale })
|
||||||
} else {
|
} else {
|
||||||
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
|
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
|
||||||
x.language
|
x.language
|
||||||
@@ -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> {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
class ShelfContent(
|
||||||
|
val history: List<MangaWithHistory>,
|
||||||
|
val favourites: Map<FavouriteCategory, List<Manga>>,
|
||||||
|
val updated: Map<Manga, Int>,
|
||||||
|
val local: List<Manga>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ShelfContent
|
||||||
|
|
||||||
|
if (history != other.history) return false
|
||||||
|
if (favourites != other.favourites) return false
|
||||||
|
if (updated != other.updated) return false
|
||||||
|
if (local != other.local) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = history.hashCode()
|
||||||
|
result = 31 * result + favourites.hashCode()
|
||||||
|
result = 31 * result + updated.hashCode()
|
||||||
|
result = 31 * result + local.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,55 @@
|
|||||||
package org.koitharu.kotatsu.shelf.domain
|
package org.koitharu.kotatsu.shelf.domain
|
||||||
|
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.*
|
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.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
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.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ShelfRepository @Inject constructor(
|
class ShelfRepository @Inject constructor(
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val trackingRepository: TrackingRepository,
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun observeShelfContent(): Flow<ShelfContent> = combine(
|
||||||
|
historyRepository.observeAllWithHistory(),
|
||||||
|
observeLocalManga(SortOrder.UPDATED),
|
||||||
|
observeFavourites(),
|
||||||
|
trackingRepository.observeUpdatedManga(),
|
||||||
|
) { history, local, favorites, updated ->
|
||||||
|
ShelfContent(history, favorites, updated, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>> {
|
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
||||||
return db.favouriteCategoriesDao.observeAll()
|
return db.favouriteCategoriesDao.observeAll()
|
||||||
.flatMapLatest { categories ->
|
.flatMapLatest { categories ->
|
||||||
@@ -26,6 +62,23 @@ class ShelfRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
private fun observeCategoriesContent(
|
||||||
categories: List<FavouriteCategoryEntity>,
|
categories: List<FavouriteCategoryEntity>,
|
||||||
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(
|
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.domain
|
||||||
|
|
||||||
|
enum class ShelfSection {
|
||||||
|
|
||||||
|
HISTORY, LOCAL, UPDATED, FAVORITES;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.shelf.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
|
||||||
@@ -23,15 +25,19 @@ 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.shelf.ui.adapter.ShelfAdapter
|
|
||||||
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener
|
|
||||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.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 ShelfFragment :
|
class ShelfFragment :
|
||||||
@@ -102,13 +108,22 @@ class ShelfFragment :
|
|||||||
val intent = when (section) {
|
val intent = when (section) {
|
||||||
is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context)
|
is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context)
|
||||||
is ShelfSectionModel.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(
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import java.util.*
|
|
||||||
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.shelf.ui.config.categories.ShelfCategoriesConfigSheet
|
|
||||||
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
|
|
||||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity
|
||||||
|
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
|
||||||
import org.koitharu.kotatsu.utils.ext.startOfDay
|
import org.koitharu.kotatsu.utils.ext.startOfDay
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class ShelfMenuProvider(
|
class ShelfMenuProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -33,18 +33,22 @@ class ShelfMenuProvider(
|
|||||||
showClearHistoryDialog()
|
showClearHistoryDialog()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_grid_size -> {
|
R.id.action_grid_size -> {
|
||||||
ShelfSizeBottomSheet.show(fragmentManager)
|
ShelfSizeBottomSheet.show(fragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_import -> {
|
R.id.action_import -> {
|
||||||
ImportDialogFragment.show(fragmentManager)
|
ImportDialogFragment.show(fragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_categories -> {
|
R.id.action_categories -> {
|
||||||
ShelfCategoriesConfigSheet.show(fragmentManager)
|
context.startActivity(ShelfSettingsActivity.newIntent(context))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ 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.shelf.ui.model.ShelfSectionModel
|
|
||||||
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
|
||||||
|
|
||||||
@@ -41,8 +42,10 @@ class ShelfSelectionCallback(
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,25 +60,34 @@ class ShelfSelectionCallback(
|
|||||||
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 ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
||||||
is ShelfSectionModel.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,4 +120,19 @@ class ShelfSelectionCallback(
|
|||||||
}
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,53 +4,63 @@ 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.core.prefs.observeAsFlow
|
||||||
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.shelf.domain.ShelfRepository
|
|
||||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
|
||||||
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.shelf.domain.ShelfContent
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
|
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 ShelfViewModel @Inject constructor(
|
class ShelfViewModel @Inject constructor(
|
||||||
repository: ShelfRepository,
|
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,
|
||||||
|
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(
|
||||||
historyRepository.observeAllWithHistory(),
|
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||||
repository.observeFavourites(),
|
networkStateObserver,
|
||||||
) { history, favourites ->
|
repository.observeShelfContent(),
|
||||||
mapList(history, favourites)
|
) { sections, isConnected, content ->
|
||||||
}.catch { e ->
|
mapList(content, sections, 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 ShelfViewModel @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) {
|
||||||
@@ -117,15 +134,39 @@ class ShelfViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
history: List<MangaWithHistory>,
|
content: ShelfContent,
|
||||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
sections: List<ShelfSection>,
|
||||||
|
isNetworkAvailable: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val result = ArrayList<ListModel>(favourites.keys.size + 1)
|
val result = ArrayList<ListModel>(content.favourites.keys.size + 3)
|
||||||
if (history.isNotEmpty()) {
|
if (isNetworkAvailable) {
|
||||||
mapHistory(result, history)
|
for (section in sections) {
|
||||||
}
|
when (section) {
|
||||||
if (favourites.isNotEmpty()) {
|
ShelfSection.HISTORY -> mapHistory(result, content.history)
|
||||||
mapFavourites(result, favourites)
|
ShelfSection.LOCAL -> mapLocal(result, content.local)
|
||||||
|
ShelfSection.UPDATED -> mapUpdated(result, content.updated)
|
||||||
|
ShelfSection.FAVORITES -> mapFavourites(result, content.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,
|
||||||
|
)
|
||||||
|
for (section in sections) {
|
||||||
|
when (section) {
|
||||||
|
ShelfSection.HISTORY -> mapHistory(
|
||||||
|
result,
|
||||||
|
content.history.filter { it.manga.source == MangaSource.LOCAL },
|
||||||
|
)
|
||||||
|
|
||||||
|
ShelfSection.LOCAL -> mapLocal(result, content.local)
|
||||||
|
ShelfSection.UPDATED -> Unit
|
||||||
|
ShelfSection.FAVORITES -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
result += EmptyState(
|
result += EmptyState(
|
||||||
@@ -134,8 +175,12 @@ class ShelfViewModel @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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,30 +188,57 @@ class ShelfViewModel @Inject constructor(
|
|||||||
destination: MutableList<in ShelfSectionModel.History>,
|
destination: MutableList<in ShelfSectionModel.History>,
|
||||||
list: List<MangaWithHistory>,
|
list: List<MangaWithHistory>,
|
||||||
) {
|
) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
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)
|
||||||
|
},
|
||||||
|
showAllButtonText = R.string.show_all,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mapUpdated(
|
||||||
|
destination: MutableList<in ShelfSectionModel.Updated>,
|
||||||
|
updated: Map<Manga, Int>,
|
||||||
|
) {
|
||||||
|
if (updated.isEmpty()) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
for ((timeAgo, subList) in groups) {
|
val showPercent = settings.isReadingIndicatorsEnabled
|
||||||
destination += ShelfSectionModel.History(
|
destination += ShelfSectionModel.Updated(
|
||||||
items = subList.map { (manga, history) ->
|
items = updated.map { (manga, counter) ->
|
||||||
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
val percent = if (showPercent) getProgress(manga.id) else PROGRESS_NONE
|
||||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
manga.toGridModel(counter, percent)
|
||||||
manga.toGridModel(counter, percent)
|
},
|
||||||
},
|
showAllButtonText = R.string.show_all,
|
||||||
timeAgo = timeAgo,
|
)
|
||||||
showAllButtonText = R.string.show_all,
|
}
|
||||||
)
|
|
||||||
|
private suspend fun mapLocal(
|
||||||
|
destination: MutableList<in ShelfSectionModel.Local>,
|
||||||
|
local: List<Manga>,
|
||||||
|
) {
|
||||||
|
if (local.isEmpty()) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
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 ShelfSectionModel.Favourites>,
|
destination: MutableList<in ShelfSectionModel.Favourites>,
|
||||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
favourites: Map<FavouriteCategory, List<Manga>>,
|
||||||
) {
|
) {
|
||||||
|
if (favourites.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
for ((category, list) in favourites) {
|
for ((category, list) in favourites) {
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
destination += ShelfSectionModel.Favourites(
|
destination += ShelfSectionModel.Favourites(
|
||||||
@@ -177,14 +249,4 @@ class ShelfViewModel @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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import 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.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.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 ShelfAdapter(
|
class ShelfAdapter(
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
@@ -40,6 +41,7 @@ class ShelfAdapter(
|
|||||||
)
|
)
|
||||||
.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))
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ class ShelfAdapter(
|
|||||||
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
|
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
|
||||||
oldItem.key == newItem.key
|
oldItem.key == newItem.key
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class ShelfSettingsActivity :
|
||||||
|
BaseActivity<ActivityShelfSettingsBinding>(),
|
||||||
|
View.OnClickListener, ShelfSettingsListener {
|
||||||
|
|
||||||
|
private val viewModel by viewModels<ShelfSettingsViewModel>()
|
||||||
|
private lateinit var reorderHelper: ItemTouchHelper
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityShelfSettingsBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.run {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material)
|
||||||
|
}
|
||||||
|
binding.buttonDone.setOnClickListener(this)
|
||||||
|
val settingsAdapter = ShelfSettingsAdapter(this)
|
||||||
|
with(binding.recyclerView) {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
adapter = settingsAdapter
|
||||||
|
reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also {
|
||||||
|
it.attachToRecyclerView(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewModel.content.observe(this) { settingsAdapter.items = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) {
|
||||||
|
viewModel.setItemChecked(item, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
|
||||||
|
reorderHelper.startDrag(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
binding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
binding.recyclerView.updatePadding(
|
||||||
|
bottom = insets.bottom,
|
||||||
|
)
|
||||||
|
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SectionsReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||||
|
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder,
|
||||||
|
): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSections(
|
||||||
|
viewHolder.bindingAdapterPosition,
|
||||||
|
target.bindingAdapterPosition,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun canDropOver(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
current: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder,
|
||||||
|
): Boolean = current.itemViewType == target.itemViewType
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||||
|
|
||||||
|
override fun isLongPressDragEnabled() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newIntent(context: Context) = Intent(context, ShelfSettingsActivity::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
|
||||||
|
class ShelfSettingsAdapter(
|
||||||
|
listener: ShelfSettingsListener,
|
||||||
|
) : AsyncListDifferDelegationAdapter<ShelfSettingsItemModel>(DiffCallback()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegatesManager.addDelegate(shelfCategoryAD(listener))
|
||||||
|
.addDelegate(shelfSectionAD(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<ShelfSettingsItemModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem is ShelfSettingsItemModel.Section && newItem is ShelfSettingsItemModel.Section -> {
|
||||||
|
oldItem.section == newItem.section
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is ShelfSettingsItemModel.FavouriteCategory && newItem is ShelfSettingsItemModel.FavouriteCategory -> {
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Any? {
|
||||||
|
return if (oldItem.isChecked == newItem.isChecked) {
|
||||||
|
super.getChangePayload(oldItem, newItem)
|
||||||
|
} else Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.core.view.updatePaddingRelative
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
fun shelfSectionAD(
|
||||||
|
listener: ShelfSettingsListener,
|
||||||
|
) =
|
||||||
|
adapterDelegateViewBinding<ShelfSettingsItemModel.Section, ShelfSettingsItemModel, ItemShelfSectionDraggableBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val eventListener = object :
|
||||||
|
View.OnTouchListener,
|
||||||
|
CompoundButton.OnCheckedChangeListener {
|
||||||
|
|
||||||
|
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||||
|
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
|
||||||
|
listener.onItemCheckedChanged(item, isChecked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.switchToggle.setOnCheckedChangeListener(eventListener)
|
||||||
|
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.setText(item.section.titleResId)
|
||||||
|
binding.switchToggle.isChecked = item.isChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shelfCategoryAD(
|
||||||
|
listener: ShelfSettingsListener,
|
||||||
|
) =
|
||||||
|
adapterDelegateViewBinding<ShelfSettingsItemModel.FavouriteCategory, ShelfSettingsItemModel, ItemCategoryCheckableMultipleBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onItemCheckedChanged(item, !item.isChecked)
|
||||||
|
}
|
||||||
|
binding.root.updatePaddingRelative(
|
||||||
|
start = binding.root.paddingStart * 2,
|
||||||
|
end = binding.root.paddingStart,
|
||||||
|
)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.root.text = item.title
|
||||||
|
binding.root.isChecked = item.isChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ShelfSection.titleResId: Int
|
||||||
|
get() = when (this) {
|
||||||
|
ShelfSection.HISTORY -> R.string.history
|
||||||
|
ShelfSection.LOCAL -> R.string.local_storage
|
||||||
|
ShelfSection.UPDATED -> R.string.updated
|
||||||
|
ShelfSection.FAVORITES -> R.string.favourites
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
|
|
||||||
|
sealed interface ShelfSettingsItemModel : ListModel {
|
||||||
|
|
||||||
|
val isChecked: Boolean
|
||||||
|
|
||||||
|
class Section(
|
||||||
|
val section: ShelfSection,
|
||||||
|
override val isChecked: Boolean,
|
||||||
|
) : ShelfSettingsItemModel {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Section
|
||||||
|
|
||||||
|
if (section != other.section) return false
|
||||||
|
if (isChecked != other.isChecked) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = section.hashCode()
|
||||||
|
result = 31 * result + isChecked.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FavouriteCategory(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
override val isChecked: Boolean,
|
||||||
|
) : ShelfSettingsItemModel {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as FavouriteCategory
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (title != other.title) return false
|
||||||
|
if (isChecked != other.isChecked) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + title.hashCode()
|
||||||
|
result = 31 * result + isChecked.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
interface ShelfSettingsListener {
|
||||||
|
|
||||||
|
fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean)
|
||||||
|
|
||||||
|
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package org.koitharu.kotatsu.shelf.ui.config
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
import org.koitharu.kotatsu.utils.ext.move
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ShelfSettingsViewModel @Inject constructor(
|
||||||
|
private val favouritesRepository: FavouritesRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val content = combine(
|
||||||
|
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||||
|
favouritesRepository.observeCategories(),
|
||||||
|
) { sections, categories ->
|
||||||
|
buildList(sections, categories)
|
||||||
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||||
|
|
||||||
|
private var updateJob: Job? = null
|
||||||
|
|
||||||
|
fun setItemChecked(item: ShelfSettingsItemModel, isChecked: Boolean) {
|
||||||
|
val prevJob = updateJob
|
||||||
|
updateJob = launchJob(Dispatchers.Default) {
|
||||||
|
prevJob?.join()
|
||||||
|
when (item) {
|
||||||
|
is ShelfSettingsItemModel.FavouriteCategory -> {
|
||||||
|
favouritesRepository.updateCategory(item.id, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShelfSettingsItemModel.Section -> {
|
||||||
|
val sections = settings.shelfSections
|
||||||
|
settings.shelfSections = if (isChecked) {
|
||||||
|
sections + item.section
|
||||||
|
} else {
|
||||||
|
if (sections.size > 1) {
|
||||||
|
sections - item.section
|
||||||
|
} else {
|
||||||
|
return@launchJob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reorderSections(oldPos: Int, newPos: Int): Boolean {
|
||||||
|
val snapshot = content.value?.toMutableList() ?: return false
|
||||||
|
snapshot.move(oldPos, newPos)
|
||||||
|
settings.shelfSections = snapshot.sections()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildList(
|
||||||
|
sections: List<ShelfSection>,
|
||||||
|
categories: List<FavouriteCategory>
|
||||||
|
): List<ShelfSettingsItemModel> {
|
||||||
|
val result = ArrayList<ShelfSettingsItemModel>()
|
||||||
|
val sectionsList = ShelfSection.values().toMutableList()
|
||||||
|
for (section in sections) {
|
||||||
|
sectionsList.remove(section)
|
||||||
|
result.addSection(section, true, categories)
|
||||||
|
}
|
||||||
|
for (section in sectionsList) {
|
||||||
|
result.addSection(section, false, categories)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<in ShelfSettingsItemModel>.addSection(
|
||||||
|
section: ShelfSection,
|
||||||
|
isEnabled: Boolean,
|
||||||
|
favouriteCategories: List<FavouriteCategory>,
|
||||||
|
) {
|
||||||
|
add(ShelfSettingsItemModel.Section(section, isEnabled))
|
||||||
|
if (isEnabled && section == ShelfSection.FAVORITES) {
|
||||||
|
favouriteCategories.mapTo(this) {
|
||||||
|
ShelfSettingsItemModel.FavouriteCategory(
|
||||||
|
id = it.id,
|
||||||
|
title = it.title,
|
||||||
|
isChecked = it.isVisibleInLibrary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ShelfSettingsItemModel>.sections(): List<ShelfSection> {
|
||||||
|
return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.shelf.ui.config.categories
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
|
|
||||||
class ShelfCategoriesConfigAdapter(
|
|
||||||
listener: OnListItemClickListener<FavouriteCategory>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(shelfCategoryAD(listener))
|
|
||||||
}
|
|
||||||
|
|
||||||
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
|
|
||||||
return oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
|
|
||||||
return oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary && oldItem.title == newItem.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: FavouriteCategory, newItem: FavouriteCategory): Any? {
|
|
||||||
return if (oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary) {
|
|
||||||
super.getChangePayload(oldItem, newItem)
|
|
||||||
} else Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.shelf.ui.config.categories
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetBaseBinding
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ShelfCategoriesConfigSheet :
|
|
||||||
BaseBottomSheet<SheetBaseBinding>(),
|
|
||||||
OnListItemClickListener<FavouriteCategory>,
|
|
||||||
View.OnClickListener {
|
|
||||||
|
|
||||||
private val viewModel by viewModels<ShelfCategoriesConfigViewModel>()
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
|
|
||||||
return SheetBaseBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
|
|
||||||
binding.buttonDone.isVisible = true
|
|
||||||
binding.buttonDone.setOnClickListener(this)
|
|
||||||
val adapter = ShelfCategoriesConfigAdapter(this)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: FavouriteCategory, view: View) {
|
|
||||||
viewModel.toggleItem(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "ShelfCategoriesConfigSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.shelf.ui.config.categories
|
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ShelfCategoriesConfigViewModel @Inject constructor(
|
|
||||||
private val favouritesRepository: FavouritesRepository,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val content = favouritesRepository.observeCategories()
|
|
||||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
private var updateJob: Job? = null
|
|
||||||
|
|
||||||
fun toggleItem(category: FavouriteCategory) {
|
|
||||||
val prevJob = updateJob
|
|
||||||
updateJob = launchJob(Dispatchers.Default) {
|
|
||||||
prevJob?.join()
|
|
||||||
favouritesRepository.updateCategory(category.id, !category.isVisibleInLibrary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.shelf.ui.config.categories
|
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
|
|
||||||
|
|
||||||
fun shelfCategoryAD(
|
|
||||||
listener: OnListItemClickListener<FavouriteCategory>,
|
|
||||||
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
|
||||||
itemView.setOnClickListener(eventListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.text = item.title
|
|
||||||
binding.root.isChecked = item.isVisibleInLibrary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,13 +8,13 @@ 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.SheetShelfSizeBinding
|
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 ShelfSizeBottomSheet :
|
class ShelfSizeBottomSheet :
|
||||||
@@ -51,9 +51,10 @@ class ShelfSizeBottomSheet :
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,29 @@ import android.content.res.Resources
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
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.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
|
|
||||||
sealed class ShelfSectionModel(
|
sealed interface ShelfSectionModel : ListModel {
|
||||||
val items: List<MangaItemModel>,
|
|
||||||
@StringRes val showAllButtonText: Int,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
abstract val key: Any
|
val items: List<MangaItemModel>
|
||||||
abstract fun getTitle(resources: Resources): CharSequence
|
|
||||||
|
@get:StringRes
|
||||||
|
val showAllButtonText: Int
|
||||||
|
|
||||||
|
val key: String
|
||||||
|
fun getTitle(resources: Resources): CharSequence
|
||||||
|
|
||||||
|
override fun toString(): String
|
||||||
|
|
||||||
class History(
|
class History(
|
||||||
items: List<MangaItemModel>,
|
override val items: List<MangaItemModel>,
|
||||||
val timeAgo: DateTimeAgo?,
|
override val showAllButtonText: Int,
|
||||||
showAllButtonText: Int,
|
) : ShelfSectionModel {
|
||||||
) : ShelfSectionModel(items, showAllButtonText) {
|
|
||||||
|
|
||||||
override val key: Any
|
override val key = "history"
|
||||||
get() = timeAgo?.javaClass ?: this::class.java
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): CharSequence {
|
override fun getTitle(resources: Resources) = resources.getString(R.string.history)
|
||||||
return timeAgo?.format(resources) ?: resources.getString(R.string.history)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -35,7 +34,6 @@ sealed class ShelfSectionModel(
|
|||||||
|
|
||||||
other as History
|
other as History
|
||||||
|
|
||||||
if (timeAgo != other.timeAgo) return false
|
|
||||||
if (showAllButtonText != other.showAllButtonText) return false
|
if (showAllButtonText != other.showAllButtonText) return false
|
||||||
if (items != other.items) return false
|
if (items != other.items) return false
|
||||||
|
|
||||||
@@ -44,28 +42,22 @@ sealed class ShelfSectionModel(
|
|||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = items.hashCode()
|
var result = items.hashCode()
|
||||||
result = 31 * result + (timeAgo?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + showAllButtonText.hashCode()
|
result = 31 * result + showAllButtonText.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String = key
|
||||||
return "hist_$timeAgo"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Favourites(
|
class Favourites(
|
||||||
items: List<MangaItemModel>,
|
override val items: List<MangaItemModel>,
|
||||||
val category: FavouriteCategory,
|
val category: FavouriteCategory,
|
||||||
showAllButtonText: Int,
|
override val showAllButtonText: Int,
|
||||||
) : ShelfSectionModel(items, showAllButtonText) {
|
) : ShelfSectionModel {
|
||||||
|
|
||||||
override val key: Any
|
override val key = "fav_${category.id}"
|
||||||
get() = category.id
|
|
||||||
|
|
||||||
override fun getTitle(resources: Resources): CharSequence {
|
override fun getTitle(resources: Resources) = category.title
|
||||||
return category.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -87,8 +79,66 @@ sealed class ShelfSectionModel(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String = key
|
||||||
return "fav_${category.id}"
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class SuggestionsViewModel @Inject constructor(
|
|||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
EmptyState(
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_suggestions,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = R.string.text_suggestion_holder,
|
textSecondary = R.string.text_suggestion_holder,
|
||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.tracker.data
|
package org.koitharu.kotatsu.tracker.data
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.MapInfo
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class TracksDao {
|
abstract class TracksDao {
|
||||||
@@ -28,9 +35,17 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
||||||
abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
|
abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@MapInfo(valueColumn = "chapters_new")
|
||||||
|
@Query("SELECT manga.*, chapters_new FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY chapters_new DESC")
|
||||||
|
abstract fun observeUpdatedManga(): Flow<Map<MangaWithTags, Int>>
|
||||||
|
|
||||||
@Query("DELETE FROM tracks")
|
@Query("DELETE FROM tracks")
|
||||||
abstract suspend fun clear()
|
abstract suspend fun clear()
|
||||||
|
|
||||||
|
@Query("UPDATE tracks SET chapters_new = 0")
|
||||||
|
abstract suspend fun clearCounters()
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun insert(entity: TrackEntity): Long
|
abstract suspend fun insert(entity: TrackEntity): Long
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package org.koitharu.kotatsu.tracker.domain
|
|||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
@@ -22,6 +21,8 @@ import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
|
|||||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||||
|
import java.util.Date
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val NO_ID = 0L
|
private const val NO_ID = 0L
|
||||||
|
|
||||||
@@ -41,6 +42,12 @@ class TrackingRepository @Inject constructor(
|
|||||||
return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } }
|
return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun observeUpdatedManga(): Flow<Map<Manga, Int>> {
|
||||||
|
return db.tracksDao.observeUpdatedManga()
|
||||||
|
.map { x -> x.mapKeys { it.key.toManga() } }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
|
||||||
val ids = mangaList.mapToSet { it.id }
|
val ids = mangaList.mapToSet { it.id }
|
||||||
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
|
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
|
||||||
@@ -91,6 +98,8 @@ class TrackingRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun clearLogs() = db.trackLogsDao.clear()
|
suspend fun clearLogs() = db.trackLogsDao.clear()
|
||||||
|
|
||||||
|
suspend fun clearCounters() = db.tracksDao.clearCounters()
|
||||||
|
|
||||||
suspend fun gc() {
|
suspend fun gc() {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tracksDao.gc()
|
db.tracksDao.gc()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui.feed
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -10,7 +10,6 @@ import androidx.fragment.app.viewModels
|
|||||||
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.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||||
@@ -23,11 +22,12 @@ 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.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter
|
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
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 org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FeedFragment :
|
class FeedFragment :
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui.feed
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -6,9 +6,9 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
|
|
||||||
@@ -37,21 +37,26 @@ class FeedMenuProvider(
|
|||||||
snackbar.show()
|
snackbar.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_clear_feed -> {
|
R.id.action_clear_feed -> {
|
||||||
MaterialAlertDialogBuilder(context)
|
CheckBoxAlertDialog.Builder(context)
|
||||||
.setTitle(R.string.clear_updates_feed)
|
.setTitle(R.string.clear_updates_feed)
|
||||||
.setMessage(R.string.text_clear_updates_feed_prompt)
|
.setMessage(R.string.text_clear_updates_feed_prompt)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.clear) { _, _ ->
|
.setCheckBoxChecked(true)
|
||||||
viewModel.clearFeed()
|
.setCheckBoxText(R.string.clear_new_chapters_counters)
|
||||||
}.show()
|
.setPositiveButton(R.string.clear) { _, isChecked ->
|
||||||
|
viewModel.clearFeed(isChecked)
|
||||||
|
}.create().show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
val intent = SettingsActivity.newTrackerSettingsIntent(context)
|
val intent = SettingsActivity.newTrackerSettingsIntent(context)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui.feed
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -17,10 +13,14 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
|||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||||
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem
|
||||||
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 org.koitharu.kotatsu.utils.ext.daysDiff
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
private const val PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -50,9 +50,12 @@ class FeedViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
fun clearFeed() {
|
fun clearFeed(clearCounters: Boolean) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
repository.clearLogs()
|
repository.clearLogs()
|
||||||
|
if (clearCounters) {
|
||||||
|
repository.clearCounters()
|
||||||
|
}
|
||||||
onFeedCleared.postCall(Unit)
|
onFeedCleared.postCall(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui.adapter
|
package org.koitharu.kotatsu.tracker.ui.feed.adapter
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.*
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
class FeedAdapter(
|
class FeedAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -33,9 +39,11 @@ class FeedAdapter(
|
|||||||
oldItem is FeedItem && newItem is FeedItem -> {
|
oldItem is FeedItem && newItem is FeedItem -> {
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui.adapter
|
package org.koitharu.kotatsu.tracker.ui.feed.adapter
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|||||||
import org.koitharu.kotatsu.databinding.ItemFeedBinding
|
import org.koitharu.kotatsu.databinding.ItemFeedBinding
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
||||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.utils.ext.isBold
|
import org.koitharu.kotatsu.utils.ext.isBold
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui.model
|
package org.koitharu.kotatsu.tracker.ui.feed.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui.model
|
package org.koitharu.kotatsu.tracker.ui.feed.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.updates
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||||
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class UpdatesActivity :
|
||||||
|
BaseActivity<ActivityContainerBinding>(),
|
||||||
|
AppBarOwner {
|
||||||
|
|
||||||
|
override val appBar: AppBarLayout
|
||||||
|
get() = binding.appbar
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivityContainerBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val fm = supportFragmentManager
|
||||||
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
|
val fragment = UpdatesFragment.newInstance()
|
||||||
|
replace(R.id.container, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
binding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.updates
|
||||||
|
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class UpdatesFragment : MangaListFragment() {
|
||||||
|
|
||||||
|
override val viewModel by viewModels<UpdatesViewModel>()
|
||||||
|
override val isSwipeRefreshEnabled = false
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance() = UpdatesFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.updates
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
|
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.toListDetailedModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class UpdatesViewModel @Inject constructor(
|
||||||
|
private val repository: TrackingRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
) : MangaListViewModel(settings) {
|
||||||
|
|
||||||
|
override val content = combine(
|
||||||
|
repository.observeUpdatedManga(),
|
||||||
|
createListModeFlow(),
|
||||||
|
) { mangaMap, mode ->
|
||||||
|
when {
|
||||||
|
mangaMap.isEmpty() -> listOf(
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_history,
|
||||||
|
textPrimary = R.string.text_history_holder_primary,
|
||||||
|
textSecondary = R.string.text_history_holder_secondary,
|
||||||
|
actionStringRes = 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> mapList(mangaMap, mode)
|
||||||
|
}
|
||||||
|
}.onStart {
|
||||||
|
loadingCounter.increment()
|
||||||
|
}.onFirst {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}.catch {
|
||||||
|
emit(listOf(it.toErrorState(canRetry = false)))
|
||||||
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
|
override fun onRefresh() = Unit
|
||||||
|
|
||||||
|
override fun onRetry() = Unit
|
||||||
|
|
||||||
|
private suspend fun mapList(
|
||||||
|
mangaMap: Map<Manga, Int>,
|
||||||
|
mode: ListMode,
|
||||||
|
): List<ListModel> {
|
||||||
|
val showPercent = settings.isReadingIndicatorsEnabled
|
||||||
|
return mangaMap.map { (manga, counter) ->
|
||||||
|
val percent = if (showPercent) historyRepository.getProgress(manga.id) else PROGRESS_NONE
|
||||||
|
when (mode) {
|
||||||
|
ListMode.LIST -> manga.toListModel(counter, percent)
|
||||||
|
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent)
|
||||||
|
ListMode.GRID -> manga.toGridModel(counter, percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class IncognitoModeIndicator @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : DefaultActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
if (activity !is AppCompatActivity) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settings.observeAsFlow(
|
||||||
|
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||||
|
valueProducer = { isIncognitoModeEnabled },
|
||||||
|
).flowOn(Dispatchers.IO)
|
||||||
|
.flowWithLifecycle(activity.lifecycle)
|
||||||
|
.onEach { updateStatusBar(activity, it) }
|
||||||
|
.launchIn(activity.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStatusBar(activity: AppCompatActivity, isIncognitoModeEnabled: Boolean) {
|
||||||
|
activity.window.statusBarColor = if (isIncognitoModeEnabled) {
|
||||||
|
ContextCompat.getColor(activity, R.color.status_bar_incognito)
|
||||||
|
} else {
|
||||||
|
activity.getThemeColor(android.R.attr.statusBarColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,14 @@ val Context.activityManager: ActivityManager?
|
|||||||
val Context.connectivityManager: ConnectivityManager
|
val Context.connectivityManager: ConnectivityManager
|
||||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
val ConnectivityManager.isNetworkAvailable: Boolean
|
||||||
|
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
activeNetwork != null
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activeNetworkInfo?.isConnectedOrConnecting == true
|
||||||
|
}
|
||||||
|
|
||||||
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
||||||
|
|
||||||
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import okio.FileNotFoundException
|
|||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.acra.ktx.sendWithAcra
|
import org.acra.ktx.sendWithAcra
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
import org.jsoup.HttpStatusException
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.CaughtException
|
import org.koitharu.kotatsu.core.exceptions.CaughtException
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
@@ -45,6 +46,12 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
|||||||
|
|
||||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||||
is NotFoundException -> resources.getString(R.string.not_found_404)
|
is NotFoundException -> resources.getString(R.string.not_found_404)
|
||||||
|
|
||||||
|
is HttpStatusException -> when (statusCode) {
|
||||||
|
in 500..599 -> resources.getString(R.string.server_error, statusCode)
|
||||||
|
else -> localizedMessage
|
||||||
|
}
|
||||||
|
|
||||||
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
|
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
|
||||||
else -> localizedMessage
|
else -> localizedMessage
|
||||||
} ?: resources.getString(R.string.error_occurred)
|
} ?: resources.getString(R.string.error_occurred)
|
||||||
|
|||||||
@@ -127,7 +127,12 @@ fun <T> RecyclerView.ViewHolder.getItem(clazz: Class<T>): T? {
|
|||||||
|
|
||||||
fun Slider.setValueRounded(newValue: Float) {
|
fun Slider.setValueRounded(newValue: Float) {
|
||||||
val step = stepSize
|
val step = stepSize
|
||||||
value = (newValue / step).roundToInt() * step
|
val roundedValue = if (step <= 0f) {
|
||||||
|
newValue
|
||||||
|
} else {
|
||||||
|
(newValue / step).roundToInt() * step
|
||||||
|
}
|
||||||
|
value = roundedValue.coerceIn(valueFrom, valueTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
val RecyclerView.isScrolledToTop: Boolean
|
val RecyclerView.isScrolledToTop: Boolean
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:alpha="0.4" android:color="?attr/colorPrimaryInverse" />
|
||||||
|
</selector>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:alpha="0.2" android:color="?colorPrimary" />
|
<item android:alpha="0.2" android:color="?attr/colorPrimaryInverse" />
|
||||||
</selector>
|
</selector>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:alpha="0.7" android:color="?attr/colorSecondaryContainer" />
|
<item android:alpha="0.7" android:color="?attr/colorSecondaryContainer" />
|
||||||
</selector>
|
</selector>
|
||||||
|
|||||||
4
app/src/main/res/color/bottom_menu_active_indicator.xml
Normal file
4
app/src/main/res/color/bottom_menu_active_indicator.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:alpha="0.4" android:color="@color/kotatsu_inversePrimary" />
|
||||||
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
|
<!-- https://stackoverflow.com/questions/54685474/theme-attributes-in-color-selector-for-api-22 -->
|
||||||
<item android:alpha="0.2" android:color="@color/kotatsu_primary" />
|
<item android:alpha="0.2" android:color="@color/kotatsu_inversePrimary" />
|
||||||
</selector>
|
</selector>
|
||||||
|
|||||||
41
app/src/main/res/drawable/ic_empty_common.xml
Normal file
41
app/src/main/res/drawable/ic_empty_common.xml
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,71 +1,53 @@
|
|||||||
<vector
|
<vector
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="192dp"
|
android:width="200dp"
|
||||||
android:height="192dp"
|
android:height="200dp"
|
||||||
android:viewportWidth="682.67"
|
android:viewportWidth="200"
|
||||||
android:viewportHeight="682.67">
|
android:viewportHeight="200">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#ededed"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m462.38,677c-0.95,-9.58 11.4,-56.66 18.63,-71 8.03,-15.92 33.35,-48.08 35.88,-45.56 0.5,0.5 33.92,111.28 35.96,119.22l0.77,3L508.28,682.67 462.95,682.67ZM156,615.11c-14.7,-2.58 -20.07,-6.87 -22.71,-18.12 -0.9,-3.84 -2.23,-7.73 -2.96,-8.65 -1,-1.26 -0.24,-1.67 3.17,-1.68 18.87,-0.07 27.32,-9.45 20.46,-22.72 -1.94,-3.75 -4.63,-5.32 -4.63,-2.7 0,3.72 -7.11,9.21 -14.7,11.34 -4.46,1.25 -8.46,3.17 -8.88,4.26 -0.81,2.12 -7.17,-7.05 -12.51,-18.06 -6.56,-13.54 7.02,-42.16 18,-37.95 3.42,1.31 2.47,-2.82 -2.39,-10.46 -8.16,-12.83 -28.7,-50.96 -29.64,-55.03 -0.51,-2.2 -1.51,-4.75 -2.24,-5.67 -0.97,-1.24 -0.48,-1.67 1.92,-1.67 4.61,0 15.79,12.85 25.74,29.6 0.74,1.25 3.84,6.35 6.89,11.35 3.04,4.99 7.48,12.67 9.86,17.06 7.67,14.18 10.37,14.67 7.19,1.32 -8.92,-37.42 -13.67,-67.33 -10.68,-67.33 4.83,0 10.19,12.2 21.09,48 4.66,15.32 11.84,29.67 21.54,43.1l9.47,13.1 0.83,-18.53c1.61,-35.75 13.16,-48.09 15.99,-17.09 6.58,71.99 -13.06,113.17 -50.83,106.53zM444.67,484.14c-11,-3.61 -21.62,-6.93 -23.61,-7.37 -11.74,-2.6 -2.05,-51.46 10.6,-53.43 2.76,-0.43 7.36,-1.41 10.23,-2.18 8.95,-2.4 9.82,-2.37 12.33,0.41 4.22,4.66 9.57,6.7 15.11,5.77 5.94,-1 13.1,2.14 22.44,9.84l5.85,4.83 -0.73,10.67c-2.41,35.09 -16.1,43.34 -52.22,31.48zM120.32,385.93c-4.48,-6.83 -9.91,-9.22 -14.98,-6.6 -2.2,1.14 -5.44,2.61 -7.21,3.29l-3.22,1.22 3.97,-11.85c3.76,-11.21 3.86,-12.09 1.93,-16.25 -13.15,-28.36 -15.66,-51.74 -5.55,-51.74 1.81,0 11.34,9.83 14.53,15 4.19,6.77 8.76,5.11 14.21,-5.15 13.53,-25.51 21.63,-25.57 21.27,-0.15 -0.14,10.24 0.51,16.12 2.6,23.61 1.54,5.49 2.79,12.41 2.79,15.4 0,2.98 1.77,11.08 3.94,17.98 4.57,14.57 4.55,14.65 -2.14,10.1 -6.92,-4.7 -9.15,-4.36 -13.37,2 -4.44,6.7 -4.5,6.57 -7.8,-15.93 -2.19,-14.94 -4.07,-20.2 -7.19,-20.2 -2.85,0 -2.48,36.4 0.42,41.09 2.35,3.8 -1.65,2.06 -4.2,-1.83zM426.03,382.7c-1.31,-1.45 -3,-2.26 -3.75,-1.79 -0.78,0.48 -0.95,0.18 -0.41,-0.7 0.57,-0.92 0.09,-1.54 -1.21,-1.54 -1.19,0 -1.79,-0.6 -1.33,-1.33 0.45,-0.73 -0.01,-1.33 -1.04,-1.33 -2.93,0 -19.63,-18.04 -19.63,-21.2 0,-2.86 1.64,-3.13 19.33,-3.13 1.83,0 4.11,0.07 5.05,0.17 1.12,0.11 2.31,4.25 3.39,11.83 0.92,6.42 2,13.92 2.4,16.67 0.83,5.73 0.49,6.01 -2.81,2.36zM323.98,346.33c-9.34,-7.29 -11.16,-11.36 -7.58,-17.02l2.93,-4.64 0.74,6c1.4,11.4 6.13,17.3 15.05,18.75l5.55,0.91 -5.56,0.17c-4.4,0.13 -6.73,-0.74 -11.12,-4.17zM345.12,345.75c3.16,-5.92 0.24,-24.53 -4.45,-28.42 -4.28,-3.55 0.4,-5.66 5.95,-2.69 4.94,2.64 7.68,23.44 3.72,28.17 -3.61,4.3 -6.94,6.18 -5.22,2.94zM107.7,286.93c-3.42,-7.84 -6.94,-18.02 -7.81,-22.63 -1.94,-10.27 1.26,-8.93 -27.89,-11.69 -13.57,-1.28 -25.12,-2.73 -25.66,-3.21 -1.05,-0.92 12.85,-12.51 21.74,-18.12 6.08,-3.84 5.85,-2.01 3.19,-26.16 -1.14,-10.32 -1.87,-18.96 -1.63,-19.2 0.24,-0.24 3.62,5.21 7.51,12.13 11.47,20.39 20.28,33.81 31.11,47.41l10.19,12.79 -1.84,21.05c-1.01,11.58 -2.03,21.24 -2.26,21.47 -0.23,0.23 -3.22,-6 -6.65,-13.84zM366,198.69c-13.11,-2.37 -20.67,-4.84 -20.67,-6.76 0,-0.53 1.22,-6.89 2.71,-14.12 4.86,-23.54 7.95,-44.94 7.95,-55.04 0,-5.44 0.23,-9.64 0.51,-9.33 0.38,0.42 1.7,10.12 2.13,15.64 0.04,0.52 6.95,-0.66 14.03,-2.39 2.93,-0.71 6.16,-1.31 7.17,-1.33 1.01,-0.02 -4.24,6.28 -11.67,13.99 -15.62,16.22 -16.29,18.25 -8.84,26.95 2.57,3 4.67,5.82 4.67,6.27 0,0.45 1.41,3.05 3.13,5.79 3.82,6.06 13.17,23.02 12.61,22.86 -0.22,-0.06 -6.41,-1.2 -13.74,-2.53z"
|
android:pathData="M81.94,186.19a154.12,154.12 0,0 0,33 -9.82l2.14,-0.93a10.54,10.54 0,0 0,0.11 3.18c0.27,0.77 1.1,1.78 1.78,1.85s1.53,-0.84 2,-1.52a11.55,11.55 0,0 0,0.91 -2.41c0.3,-0.71 0.7,-1.37 1.12,-2.19a43.76,43.76 0,0 0,3.64 3.11,2.27 2.27,0 0,0 2.07,0.16c0.4,-0.32 0.29,-1.36 0.27,-2.07a1.7,1.7 0,0 0,-0.46 -1.06c-3.23,-3.23 -1.86,-5.71 0.72,-8.76 8,-14.16 8.77,-22.84 5.83,-36.44 -3.51,-16.2 -6.14,-32.31 -15.74,-46.14 -3,-4.39 -4.83,-9.69 -7,-14.66a14.64,14.64 0,0 1,-0.71 -4.58c-0.83,-10.78 -3.1,-20.89 -4.48,-31.6 -0.59,-4.59 -3,-10.25 -5.23,-14.37 -2.85,-5.19 -13.68,-8.4 -17.09,-3.54 -3.66,5.21 -3.42,14.21 -4.93,20.39C78,42.44 78.64,49.65 76.06,57.56a73.93,73.93 0,0 0,-15.12 3.65,99.69 99.69,0 0,0 -5,-16.75C52.58,36 48.67,27.83 41.4,21.84c-0.53,-0.43 -1,-0.9 -1.59,-1.29 -5.14,-3.64 -9.54,-2.46 -10.72,3.69A112.24,112.24 0,0 0,27 45.71C27,54.84 28.12,64 28.7,73.1a8,8 0,0 1,-0.28 3.13C25.52,84.29 25,92.61 25.81,101a86.56,86.56 0,0 1,0.12 18.63c-1.4,12.31 2.45,23.73 7.59,34.71 4.21,9 9.53,17.26 16.72,24.17C53,181.22 55.82,184 59.4,186c1.84,1 4.4,0.6 4.46,1s-1,0.93 -3.5,3c-1.4,1.19 -1.82,1.67 -1.74,2.19s1.2,0.58 1.82,0.78a0.7,0.7 0,0 0,0.64 -0.11A12.45,12.45 0,0 0,64.29 190,7.41 7.41,0 0,0 66,187.39a22.49,22.49 0,0 0,-0.87 5.25c0,0.7 2.29,0.56 2.62,-0.44 0.44,-1.31 -0.44,-3.06 1.31,-4.37 0,0 -1,4.34 0.44,5.24a1.9,1.9 0,0 0,2.18 -0.43c1.09,-1.22 -0.61,-3.32 0,-4.81S74.78,186.52 81.94,186.19ZM79.47,61.68c0.32,-1 0.65,-1.9 0.89,-2.82C82.08,52.09 81.57,45.8 83,38.3c1.71,-8.84 0.81,-15.64 4.65,-21.69 2,-3.2 10.42,0.16 12,3.54a50.1,50.1 0,0 1,4 13.44c1.23,8.86 4.31,17.41 4.43,26.35a41.43,41.43 0,0 0,8.22 24.79,59.62 59.62,0 0,1 9.62,18.34c4.41,14.18 8.3,27.09 7.86,41.51 -1.31,10 -2.88,18.68 -15.27,26 -14.5,8.51 -30.57,12.59 -47.41,13.36a22.35,22.35 0,0 1,-17 -6.25c-8.42,-7.89 -14.4,-17.39 -19,-27.82 -4.3,-9.84 -6.91,-20 -5.89,-31a76.17,76.17 0,0 0,0.22 -14.24c-0.94,-9.72 -1,-19.26 2.55,-28.55a6.05,6.05 0,0 0,0.09 -3.14,134.53 134.53,0 0,1 -1.71,-31.74A121.63,121.63 0,0 1,32.2 25.26c0.76,-4 2.8,-4.63 5.72,-1.92a52,52 0,0 1,8.61 9.79,77.09 77.09,0 0,1 11,28.76c0.28,1.67 0.88,2.9 2.88,2.06A34.41,34.41 0,0 1,79.47 61.68Z"
|
||||||
android:strokeWidth="1.33333" />
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="?colorOnSurface" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#d7d7d7"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m366.82,652.33c0.09,-16.68 0.37,-29.43 0.64,-28.33 0.7,2.88 5.87,51.79 5.87,55.53 0,2.46 -0.72,3.14 -3.33,3.14h-3.33zM286,451.86c-22.24,-3.2 -46.33,-12.87 -57.7,-23.14 -11.14,-10.07 -11.32,-11.41 -1.25,-9.25 9.93,2.13 18.29,2.38 18.29,0.54 0,-0.73 -0.55,-1.33 -1.22,-1.33 -4.8,0 -29.91,-27.74 -35.49,-39.21 -7.92,-16.29 -8.12,-17.92 -3.99,-32.12 5.82,-19.96 5.79,-32.65 -0.12,-62.44 -2.68,-13.5 -2.83,-13.51 9.26,0.7l10.34,12.14 -3.56,3.72c-7.47,7.79 -1.42,13.63 13.56,13.09 7.21,-0.26 9.2,0.27 16.76,4.5 31.78,17.77 42.62,16.6 47.91,-5.18 0.87,-3.58 2.66,-7.71 3.97,-9.18 1.31,-1.47 3.43,-6.42 4.7,-11 1.27,-4.58 3.06,-8.33 3.97,-8.33 2.44,0 8.57,-7.77 8.57,-10.87 0,-2.36 0.37,-2.23 3.18,1.1 1.75,2.07 9.86,9.75 18.04,17.06 20.77,18.58 26.54,43.03 19.59,83.04 -2.83,16.31 -2.85,16.21 2.53,11 5.77,-5.59 5.79,-4.92 0.22,7.14 -8.03,17.39 -26.79,42.18 -40.94,54.11l-6.42,5.42 -11.09,-0.16c-6.1,-0.09 -14.69,-0.68 -19.09,-1.32zM315.51,435.34c6.25,-4.1 8.97,-12.9 7.97,-25.85 -0.95,-12.33 -1.25,-13.22 -3.78,-11.16 -0.93,0.76 -4.78,1.79 -8.54,2.29 -5.37,0.72 -7.79,1.9 -11.26,5.48 -3.86,3.98 -5.26,4.56 -11,4.56h-6.58l0.9,5.63c2.8,17.54 19.53,27.4 32.28,19.05zM223.98,367.15c6,-2.73 23.77,-3.96 29.29,-2.04 4.01,1.4 4.18,1.32 3.33,-1.49 -1.35,-4.46 -2.19,-4.96 -10.03,-5.86 -3.98,-0.46 -7.63,-1.44 -8.11,-2.18 -0.54,-0.83 -1.72,-0.5 -3.1,0.87 -1.22,1.22 -3.22,2.22 -4.45,2.23 -4.45,0.03 -16.28,6.54 -17.44,9.6 -1.62,4.29 -0.38,5.57 2.99,3.09 1.58,-1.16 4.96,-3.07 7.52,-4.23zM340,349.11c0,-0.86 0.39,-1.16 0.87,-0.69 0.48,0.48 1.72,-0.17 2.77,-1.44 1.04,-1.27 1.44,-1.44 0.89,-0.38 -0.63,1.21 0.44,0.9 2.9,-0.85 2.15,-1.53 4.51,-2.83 5.24,-2.88 0.73,-0.05 1.79,-2.97 2.34,-6.48 2.81,-17.79 -4.73,-30.41 -17.65,-29.51 -5.95,0.41 -6.7,0.17 -6.71,-2.2 -0.05,-6.19 -2.39,-6.5 -4.12,-0.55 -1.05,3.59 -4.48,9.01 -8.78,13.89 -7.19,8.15 -9.09,14 -5.28,16.27 0.98,0.59 2.91,2.8 4.28,4.93 5.47,8.48 23.26,16.06 23.26,9.92zM409.63,443.64 L405.33,439.21 405.31,419.94c-0.01,-11.96 -1.02,-26.34 -2.67,-37.9 -1.46,-10.25 -2.65,-19.53 -2.65,-20.62 0,-1.12 3.39,1.36 7.74,5.66 4.26,4.21 8.25,7.19 8.88,6.62 0.63,-0.57 0.71,-0.29 0.18,0.63 -0.6,1.04 -0.15,1.67 1.2,1.67 1.19,0 1.79,0.6 1.33,1.33 -0.45,0.73 -0.01,1.33 0.99,1.33 3.65,0 9.85,8.43 10.23,13.91 0.2,2.88 0.59,6.42 0.87,7.87 0.28,1.45 0.9,6.95 1.38,12.24l0.87,9.61 -4.21,1.47c-4.47,1.56 -7.08,6.11 -10.07,17.57 -1.94,7.45 -4.24,8 -9.75,2.31z"
|
android:pathData="M89.05,76.37a14.46,14.46 0,1 0,18.64 8.55A14.49,14.49 0,0 0,89.05 76.37ZM90.46,79.87c5.23,-1.92 11.73,1.18 13.62,6.5s-1.14,11.68 -6.23,13.49a10.66,10.66 0,0 1,-7.39 -20Z"
|
||||||
android:strokeWidth="1.33333" />
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="?colorOnSurface" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#afafaf"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m135.96,681c-0.02,-0.92 -0.34,-6.17 -0.71,-11.67l-0.68,-10 3.95,7.33c2.17,4.03 5.76,9.28 7.97,11.67l4.03,4.33h-7.26c-4.9,0 -7.27,-0.54 -7.3,-1.67zM224.73,677.86c-0.73,-3.89 -0.26,-5.73 2.5,-9.71 4.81,-6.94 7.84,-16.63 7.03,-22.51 -8.37,-61.15 -12.36,-73.12 -24.62,-74.01 -3.46,-0.25 -3.85,-0.64 -2.74,-2.73 1.88,-3.52 2.81,-34.06 1.49,-49.05l-1.13,-12.82 4.03,-1.57 4.03,-1.57 -4.33,-1.87c-2.78,-1.2 -4.33,-2.81 -4.33,-4.5 0,-7.77 -4.25,-8.99 -9.42,-2.7l-3.42,4.16 -4.31,-4.57c-2.37,-2.52 -5.22,-6.32 -6.33,-8.46 -1.11,-2.13 -3.8,-6.29 -5.98,-9.24 -8.2,-11.09 -16.77,-22.9 -17.63,-24.3 -1.06,-1.72 1.91,-4.9 20.34,-21.72 8.3,-7.57 14.1,-11.98 14.44,-10.96 4.04,12.11 24.92,48.52 39.86,69.49l10.35,14.53 9.19,0.52 9.19,0.52 6.77,7.28c16,17.2 38.49,39.58 43.49,43.28 7.57,5.6 5.8,8.08 -4.56,6.4 -9.39,-1.52 -16.91,12.64 -10.02,18.88 4.51,4.08 13.8,4.3 16.87,0.4 1.53,-1.94 4.67,-3.67 7.7,-4.24l5.13,-0.96 -0.57,4.42c-0.31,2.43 -3.83,26.47 -7.82,53.42l-7.25,49 -43.52,0 -43.52,0 -0.9,-4.8zM283.33,540.48c-10.27,-5.57 -24.95,-13.63 -32.63,-17.9 -16.57,-9.22 -16.28,-9.3 -9.95,2.74 8.16,15.53 9.53,16.52 26.58,19.19 8.07,1.26 18.27,3.1 22.67,4.09 13.2,2.96 12.76,2.44 -6.67,-8.11zM364,681.33c0,-7.26 -9.96,-85.3 -12.76,-100 -1.05,-5.5 -1.91,-10.09 -1.91,-10.2 -0,-0.34 8.33,-3.13 9.36,-3.13 1.18,0 1.93,4.15 5.69,31.33 3.16,22.82 3.81,83.33 0.9,83.33 -0.71,0 -1.29,-0.6 -1.29,-1.34zM445.58,673.67c-6.74,-11.66 -60.24,-111.93 -60.24,-112.92 0,-0.41 2.4,-0.75 5.33,-0.75 11.75,0 14.79,-13.58 4.84,-21.64 -3.53,-2.86 -7.8,-2.97 -12.91,-0.33 -5.05,2.61 -5.07,4.95 0.27,-40.7 5.03,-43.05 2.99,-36.97 13.3,-39.74l8.49,-2.29 0.67,-7.78 0.67,-7.78 3.19,3.41c1.76,1.88 3.8,3.04 4.54,2.58 0.76,-0.47 0.93,-0.17 0.39,0.71 -0.54,0.88 -0.17,1.54 0.87,1.54 2.58,0 1.88,6.38 -1.4,12.82 -1.49,2.92 -3.15,7.67 -3.69,10.56 -0.58,3.12 -3.52,8.56 -7.21,13.38 -5.85,7.63 -24.11,48.93 -22.4,50.64 0.6,0.6 17.15,-18.13 25.99,-29.4 2.69,-3.43 4.51,-7.59 5.1,-11.67 1.41,-9.76 4.53,-8.16 5.36,2.76 0.26,3.43 2,12.16 3.86,19.4 2.38,9.25 3.33,16.24 3.19,23.52 -0.29,15.19 6.64,23.91 23.64,29.77 4.96,1.71 5.25,2.12 5.48,7.68 0.14,3.23 0.92,26.57 1.74,51.87 0.82,25.3 1.81,47.65 2.19,49.67 1.54,8.05 -5.43,4.75 -11.27,-5.33zM553.96,681c-0.34,-0.92 -8.66,-28.92 -18.49,-62.23l-17.87,-60.56 3.48,-5.44c1.91,-2.99 3.64,-5.68 3.84,-5.96 0.2,-0.29 1.39,0.31 2.64,1.33 1.96,1.59 2.13,1.59 1.16,-0.02 -0.77,-1.27 -0.63,-1.57 0.41,-0.92 0.85,0.52 1.54,1.76 1.54,2.76 0,0.99 5.77,13.3 12.83,27.35 15.67,31.2 14.88,27.74 15.97,70.35l0.9,35h-2.89c-1.59,0 -3.17,-0.75 -3.51,-1.67zM338.17,541.74c-1.9,-4.99 -6.76,-17.94 -10.81,-28.78 -15.23,-40.76 -18.34,-45.25 -29.06,-41.95 -11.17,3.44 -19.73,4.32 -21.07,2.16 -0.76,-1.24 -0.39,-2.14 1.15,-2.75 1.34,-0.53 -0.29,-0.95 -3.92,-1l-6.2,-0.09 -0.96,-8.33c-0.53,-4.58 -1.44,-9.59 -2.02,-11.12l-1.06,-2.79 10.22,2.49c5.62,1.37 17.25,2.86 25.84,3.32l15.62,0.84 8.55,-7.51 8.55,-7.51 0.83,4.14c1.52,7.62 1.06,8.85 -3.93,10.5 -2.62,0.86 -4.51,1.82 -4.21,2.12 0.3,0.3 2.99,-0.31 5.98,-1.37 5.2,-1.84 6.98,-1.21 6.98,2.45 0,0.19 -2.4,1.14 -5.33,2.1 -5.55,1.83 -5.74,2.23 -5.38,11.33 0.45,11.56 8.84,40.33 19.25,66 4.95,12.21 4.91,12.61 -1.38,13.93 -4.15,0.87 -4.22,0.79 -7.64,-8.19zM182.76,533.33c-3.25,-4.77 -6.43,-8.88 -7.07,-9.13 -0.64,-0.26 2.32,-2.12 6.57,-4.15 4.25,-2.03 7.75,-3.62 7.76,-3.53 0.02,0.08 -0.28,5.85 -0.67,12.82l-0.69,12.67zM109.29,300.42c-2.55,-4.14 -5.55,-9.07 -6.67,-10.97 -1.12,-1.9 -3.67,-5.99 -5.67,-9.09 -1.99,-3.1 -3.62,-6.22 -3.62,-6.92 0,-1.94 -1.57,-2.58 -11.54,-4.67 -10.66,-2.24 -37.8,-15.52 -37.8,-18.5 0,-0.6 0.49,-0.79 1.09,-0.42 0.6,0.37 10.35,1.6 21.67,2.74 11.32,1.13 23.07,2.39 26.12,2.8l5.54,0.74 0.95,8.27c0.93,8.1 12.74,37.74 15.89,39.88 0.94,0.64 0.95,1.38 0.03,2.31 -0.93,0.93 -2.84,-1.05 -5.99,-6.16zM426.77,295.35c-11.15,-5.92 -12.35,-7.36 -16.73,-20.02 -6.48,-18.74 -16.36,-35.57 -29.03,-49.43l-6.76,-7.4 6.54,-6.83c3.6,-3.76 7.25,-7.62 8.11,-8.59 3.42,-3.83 5.88,-0.11 7.66,11.57 1.32,8.7 3.25,13.36 7.7,18.56 8.64,10.09 13.05,22.32 8.97,24.84 -2.51,1.55 4.29,7.29 13.77,11.6 9.93,4.52 10.18,4.86 6.33,8.33 -3.5,3.16 -3.51,11.09 -0.02,17.29 1.45,2.59 2.5,4.68 2.33,4.66 -0.17,-0.03 -4.16,-2.1 -8.87,-4.6zM116.87,284.33c0.62,-7.88 1.13,-17.43 1.13,-21.22 0,-7.74 0.76,-7.64 6.68,0.9 2.81,4.05 3.32,5.71 2.17,7.09 -0.82,0.99 -1.1,2.2 -0.62,2.68 0.48,0.48 0.11,0.88 -0.82,0.88 -0.96,0 -1.37,0.87 -0.93,2 0.42,1.1 0.12,2 -0.67,2 -0.79,0 -1.09,0.9 -0.67,2 0.42,1.1 0.12,2 -0.67,2 -0.79,0 -1.09,0.9 -0.67,2 0.42,1.1 0.12,2 -0.67,2 -0.79,0 -1.09,0.9 -0.67,2 0.42,1.1 0.12,2 -0.67,2 -0.79,0 -1.09,0.9 -0.67,2 0.42,1.1 0.12,2 -0.67,2 -0.79,0 -1.09,0.9 -0.67,2 0.42,1.1 0.13,2 -0.65,2 -0.99,0 -1.08,-4.38 -0.3,-14.33zM356.95,162.79c-3.72,-6.3 -2.81,-8.11 11.81,-23.41 15.61,-16.34 16.08,-15.7 4.67,6.41 -11.77,22.8 -12.56,23.61 -16.47,17z"
|
android:pathData="M51.32,106a13.27,13.27 0,1 0,-17.15 -7.9A13.29,13.29 0,0 0,51.32 106ZM37.9,96.78a9.68,9.68 0,0 1,5.52 -12,9.39 9.39,0 1,1 6.44,17.64A9.57,9.57 0,0 1,37.9 96.78Z"
|
||||||
android:strokeWidth="1.33333" />
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="?colorOnSurface" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#777777"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m302.67,583.09c-7.71,-2.96 -9.05,-11.58 -2.77,-17.85 6.7,-6.7 14.71,-4.18 17.62,5.53 2.7,9.02 -5.57,15.88 -14.85,12.32zM339.35,569.82c-5.5,-5.5 -4.34,-14.72 2.29,-18.15 12.59,-6.51 21.23,-2.69 21.57,9.53 0.18,6.68 -18.9,13.57 -23.86,8.61zM381,557c-7.86,-8.09 -2.53,-21.08 8.6,-20.97 8.55,0.08 15.06,13.58 9.93,20.6 -3.38,4.63 -14.19,4.84 -18.53,0.37zM432,496.14c-22.95,-7.47 -28.2,-17.13 -18.68,-34.39 2.42,-4.39 2.7,-4.58 2.04,-1.38 -3,14.56 4.02,25.66 20.56,32.5 14.59,6.03 11.77,8.38 -3.92,3.27zM158.43,430.32c-26.78,-28.97 -32.81,-37.6 -34.51,-49.38 -1.53,-10.58 -1.56,-29.06 -0.05,-31.45 2.44,-3.85 3.92,0.44 6.88,19.82 3.38,22.16 3.42,22.23 8.42,14.68 4.3,-6.49 6.99,-6.72 14.92,-1.27 3.25,2.24 5.91,3.81 5.91,3.5 0,-0.31 -1.85,-6.49 -4.11,-13.73 -2.26,-7.24 -4.34,-16.62 -4.62,-20.84 -0.28,-4.22 -1.51,-10.82 -2.74,-14.67 -1.54,-4.81 -2.28,-12.03 -2.38,-23.12 -0.24,-27.24 -8.2,-27.72 -22.14,-1.32 -3.63,6.87 -5.26,8.76 -7.33,8.48 -2.87,-0.39 -4.01,-10.11 -1.68,-14.36 0.6,-1.1 3.77,-10.06 7.05,-19.92 6.71,-20.2 6.87,-18.73 -3.32,-29.97 -9.71,-10.71 -18.29,-23.17 -34.05,-49.45l-16,-26.67 -0.43,6.67c-0.24,3.67 0.6,14.06 1.86,23.09 2.1,15.09 2.11,16.6 0.14,18.57 -1.8,1.8 -2.24,1.84 -2.75,0.24 -3.48,-10.84 -6.21,-20.5 -7.49,-26.57 -0.86,-4.03 -2.05,-9.43 -2.64,-12 -2,-8.58 -4.5,-26.63 -3.79,-27.33 0.38,-0.38 3.55,0.56 7.04,2.11 5.8,2.57 21.58,7.42 28.07,8.64 1.47,0.28 8.37,1.39 15.33,2.47 23.59,3.68 40.18,8.83 59.11,18.35 8.16,4.1 8.35,4.08 14.09,-1.34 13.08,-12.38 35.87,-22.27 68.14,-29.56l12.67,-2.86 4.64,-6.9c4.69,-6.98 10.19,-14 18.7,-23.89 6.25,-7.26 20.26,-18.32 30.04,-23.72 10.39,-5.73 14.36,-9.46 21.27,-19.96 9.53,-14.48 16.95,-18.36 18.61,-9.73 5.47,28.41 2.18,84.19 -6.72,114.01 -0.77,2.57 5.65,5.85 14.09,7.2 4.45,0.71 5.03,1.31 6.47,6.71 1.19,4.44 4.18,8.68 11.97,16.95 18.44,19.57 28.01,36.56 36.91,65.53 5.12,16.66 12.3,30.62 20.3,39.48l4.62,5.11 -9.45,-3.22c-5.2,-1.77 -9.45,-2.93 -9.45,-2.58 0,0.35 -1.2,-0.01 -2.67,-0.79 -2.36,-1.26 -2.68,-1 -2.77,2.29 -0.06,2.04 -0.32,4.85 -0.58,6.25 -0.26,1.39 -0.62,5.29 -0.8,8.67 -0.18,3.37 -0.57,6.86 -0.88,7.75 -0.61,1.77 -14.94,1.81 -14.96,0.05 -0.01,-0.55 -1.18,-1 -2.6,-1 -1.42,0 -2.92,-0.54 -3.34,-1.21 -2.98,-4.82 -14.35,5.89 -13.51,12.72 0.59,4.81 0.02,6.46 -4.49,12.89 -2.84,4.06 -4.92,7.73 -4.62,8.15 0.3,0.43 0.14,0.46 -0.36,0.08 -0.5,-0.38 -2.99,1.27 -5.53,3.67 -3.43,3.24 -4.37,3.67 -3.63,1.69 10.83,-29.21 3.94,-76.21 -13.39,-91.33 -5.46,-4.77 -13.34,-12.06 -17.5,-16.21l-7.57,-7.54 -2.29,5.12c-1.27,2.85 -4,6 -6.15,7.11 -3.06,1.58 -4.19,3.45 -5.48,9.05 -0.9,3.88 -2.73,8.27 -4.07,9.76 -1.34,1.49 -3.15,5.64 -4.02,9.22 -5.43,22.34 -16.03,23.81 -46.75,6.47 -7.89,-4.45 -9.65,-4.91 -18.89,-4.95 -14.97,-0.06 -19.88,-5.16 -12.04,-12.5l3.77,-3.53 -11.98,-13.94c-8.43,-9.81 -11.72,-12.88 -11.09,-10.37 8.08,32.18 7.84,64.19 -0.61,82.74 -4.53,9.94 12.96,37.58 36.8,58.16l6.67,5.75 -10.67,-0.92c-5.87,-0.5 -11.97,-1.03 -13.56,-1.17 -1.59,-0.14 -2.67,-0.47 -2.41,-0.73 0.26,-0.26 -0.1,-1.16 -0.79,-2 -0.7,-0.84 -2.54,-3.21 -4.09,-5.28 -2.49,-3.32 -3.4,-3.66 -7.85,-2.91 -3.05,0.51 -5.38,0.29 -5.91,-0.58 -1.73,-2.81 -3.49,-1.43 -1.92,1.51 1.34,2.51 1.15,3.21 -1.29,4.83 -1.57,1.04 -2.86,2.59 -2.86,3.45 0,1.42 -4.66,6.18 -19.38,19.79l-5.38,4.97 -10.15,-10.99zM298,433.58c-2.93,-1.37 -6.15,-3.48 -7.15,-4.69 -1,-1.21 -2.34,-1.87 -2.98,-1.48 -0.64,0.4 -0.77,-0.31 -0.29,-1.57 0.59,-1.54 0.37,-1.98 -0.67,-1.33 -1.01,0.62 -1.27,0.24 -0.75,-1.11 0.44,-1.14 0.28,-2.07 -0.35,-2.07 -0.63,-0 -1.27,-1.8 -1.42,-4 -0.32,-4.54 -0.2,-4.58 5.86,-2.39 4.32,1.56 4.62,1.43 10.81,-4.67 7.87,-7.75 18.42,-6.18 19,2.83 0.03,0.49 0.55,3.46 1.16,6.59 0.62,3.22 0.59,6.01 -0.08,6.43 -0.65,0.4 -0.84,1.29 -0.41,1.97 0.42,0.68 0.13,1.24 -0.64,1.24 -0.78,0 -1.41,1 -1.41,2.22 0,1.22 -0.39,1.83 -0.88,1.34 -0.48,-0.48 -1.56,-0.31 -2.4,0.39 -3.24,2.69 -11.94,2.84 -17.39,0.29z"
|
android:pathData="M96.25,93.49c1.35,-0.59 3,-1.64 2.15,-4.11s-3.71,-4 -5.71,-3.35c-2.22,0.78 -3.79,4.16 -2.91,6.25C90.55,94.09 93,94.69 96.25,93.49Z"
|
||||||
android:strokeWidth="1.33333" />
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="?colorOnSurface" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#5a5a5a"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m457.25,681.33c-0.07,-1.1 -1.13,-26.32 -2.35,-56.05l-2.23,-54.05 6.62,1.72c14.2,3.69 28.88,1.58 45.38,-6.5 4.95,-2.42 6.94,-3 5.24,-1.52 -23.22,20.19 -41.19,60.08 -49.16,109.1 -0.98,6.01 -3.27,10.82 -3.49,7.31zM147.18,620.22c-2.28,-1.44 -5.46,-4.46 -7.05,-6.7l-2.91,-4.08 6.39,2.94c18.12,8.33 47.05,-0.67 54.65,-17.01l1.87,-4.02 -0.63,6c-0.65,6.15 -10.27,17.33 -14.92,17.33 -1.01,0 -5.16,1.52 -9.23,3.39 -10.29,4.71 -22.64,5.66 -28.16,2.17zM126.93,583.73c-4.38,-4.38 -1.44,-7.83 9.31,-10.92 6.99,-2 11.71,-5.69 13.5,-10.55 1.01,-2.74 3.69,0.9 5.02,6.84 2.64,11.74 -19.22,23.23 -27.83,14.63zM277.33,545.94c-8.43,-1.67 -17.13,-3.1 -19.33,-3.19 -6.07,-0.23 -22,-19.73 -22,-26.93 0,-0.85 59.11,30.83 60.93,32.66 1.3,1.3 -2.93,0.76 -19.6,-2.54zM380.52,534.09c7.68,-21.64 16.57,-41.4 21.8,-48.47l6.15,-8.31 1.79,5.02c4.15,11.65 1.16,18.23 -18.67,41.02 -11.84,13.61 -12.19,13.95 -11.06,10.75zM124.52,511.34c-3.5,-4.76 -6.51,-9.65 -6.68,-10.87 -0.18,-1.21 -1.11,-2.7 -2.08,-3.32 -0.97,-0.61 -1.3,-1.12 -0.75,-1.13 0.56,-0.01 -2.59,-7.21 -7,-16 -13.42,-26.76 -10.91,-23.59 8.41,10.65 5.59,9.9 11.71,20.55 13.61,23.67 5.32,8.72 1.62,6.71 -5.51,-3zM146.09,512.62c-2.2,-3.36 -4.85,-13.56 -7.43,-28.62 -2.56,-14.95 -3,-18.11 -3.2,-22.92 -0.11,-2.71 -0.55,-5.27 -0.97,-5.7 -0.81,-0.81 0.08,-16.97 0.99,-17.93 0.29,-0.31 0.53,2.81 0.53,6.93 0,7.96 3.09,25.41 9.46,53.5 4.15,18.29 4.23,20.24 0.63,14.75zM456,499.33c-4.03,-0.98 -7.63,-1.73 -8,-1.67 -2.43,0.36 -22,-9.22 -26.33,-12.89 -10.92,-9.25 -4.69,-9.26 22.68,-0.03 33.86,11.41 49.59,3.77 52.62,-25.54 1.12,-10.87 3.04,-10.08 3.04,1.25 0,30.84 -16.6,45.51 -44,38.89zM321.6,421.09c-1.18,-16.01 -10.35,-20.86 -20.5,-10.86l-6.33,6.23 -5.39,-1.78c-7.01,-2.31 -6.82,-3.35 0.63,-3.35 5.25,0 6.6,-0.64 10.6,-5 3.87,-4.23 5.43,-5 10.07,-5 3.02,0 6.63,-0.61 8.04,-1.36 3.39,-1.82 4.22,1.57 3.61,14.71 -0.26,5.49 -0.59,8.37 -0.73,6.4z"
|
android:pathData="M48.33,97.4A4.61,4.61 0,0 0,51.12 92c-0.64,-1.89 -3.84,-2.79 -6.34,-1.78a4,4 0,0 0,-2 5.5C43.66,97.63 45.39,98.53 48.33,97.4Z"
|
||||||
android:strokeWidth="1.33333" />
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="?colorOnSurface" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#333333"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m168.3,669.31c-10.12,-1.48 -21.12,-7.25 -25.01,-13.13 -9.96,-15.05 -17.41,-44.19 -14.8,-57.85 1.25,-6.52 2.59,-6.32 4.49,0.67 5.29,19.49 16.83,26.48 35.74,21.63 19.61,-5.04 29.41,-13.01 31.27,-25.43 4.9,-32.86 18.91,-30.93 25.76,3.56 8.76,44.09 6.83,50.86 -18.08,63.41 -14.63,7.37 -24.96,9.25 -39.37,7.15zM460.67,560.88c-17.4,-5.76 -36.62,-22.28 -36.69,-31.53 -0.01,-1.85 -1.5,-9.06 -3.31,-16.02 -1.8,-6.97 -3.29,-14.88 -3.31,-17.59l-0.03,-4.93 7.12,3.26c8.51,3.9 18.05,5.87 21.76,4.5 1.48,-0.55 6.23,-0.16 10.57,0.86 25.84,6.08 43.22,-8.5 43.22,-36.24 0,-8.3 0.67,-8.65 6.22,-3.22 3.09,3.02 4.25,5.58 4.86,10.66 1.27,10.68 4.86,24.12 8.51,31.81 13.66,28.77 -26.94,69.04 -58.93,58.44zM172.82,518.57c-1.33,-2.25 -2.21,-4.27 -1.95,-4.49 5.85,-5.07 22.7,-15.29 21.93,-13.31 -0.55,1.41 -1.32,5.32 -1.73,8.7 -0.7,5.87 -1.04,6.29 -7.93,9.67 -8.7,4.27 -7.43,4.33 -10.32,-0.56zM264.48,506c-0.58,-1.5 -3,-2 -9.76,-2h-8.99l-10.75,-15c-13.46,-18.77 -25.36,-38.71 -34.44,-57.67l-7.02,-14.67 2.74,-2.93c1.51,-1.61 2.44,-3.71 2.07,-4.67 -0.49,-1.28 1.09,-1.73 6.08,-1.73h6.74l4.39,7.52c7.88,13.48 21.59,23.87 39.68,30.08 9.32,3.2 10.1,3.75 10.1,7.16 0,2.03 0.41,4.77 0.92,6.08 0.81,2.1 0.3,2.02 -4.21,-0.63 -4.41,-2.6 -5.65,-2.8 -8.91,-1.45 -2.08,0.86 -5.29,1.28 -7.13,0.93 -1.84,-0.35 -3.34,-0.48 -3.34,-0.29 0,0.92 9.91,17.53 19.05,31.94 9.85,15.52 10.05,16.01 6.86,16.42 -1.81,0.23 -2.94,0.98 -2.52,1.67 0.42,0.68 0.41,1.24 -0.02,1.24 -0.43,0 -1.13,-0.9 -1.55,-2zM386.92,455c-1.54,-4.03 -4.21,-28.77 -4.35,-40.33 -0.16,-13 -1.81,-13.42 -4.54,-1.17 -1.37,6.14 -2.99,12.57 -3.6,14.28l-1.11,3.11 -4.09,-3.44c-5.25,-4.41 -5.45,-4.38 -8.59,1.47 -8.12,15.13 -11.41,17.32 -15.53,10.34l-2.8,-4.75 -3.82,3.66c-6.03,5.78 -4.99,0.07 1.22,-6.69 11.41,-12.41 26.36,-37.15 29.08,-48.12 0.54,-2.18 3.06,-6.86 5.6,-10.4 4.11,-5.72 4.53,-7.07 3.81,-12.3 -0.92,-6.75 0.75,-9.26 7.11,-10.66 2.21,-0.48 4.01,-1.39 4.01,-2.01 0,-0.62 1.72,-0.27 3.82,0.78 6.09,3.04 10.78,36.92 10.82,78.17l0.03,27.62 -6.91,2.05c-8.86,2.63 -8.53,2.68 -10.17,-1.62zM326.88,344.01c-6.07,-6.07 -8.16,-20.99 -3.71,-26.55 8.42,-10.53 19.69,-2.24 22.79,16.77 1.08,6.6 -1.38,13.13 -3.51,9.33 -3.35,-5.98 -11.72,-4.33 -9.74,1.91 1.18,3.7 -1.28,3.09 -5.83,-1.47z"
|
android:pathData="M121.32,84.4 L128.48,62c0.93,-2.88 1.67,-9 5.08,-9.14 2.75,-0.11 7.08,2.25 9.73,3.16 3.51,1.21 7,2.47 10.53,3.64 3.31,1.1 8,1.52 11,3.33 1.87,1.15 3.29,4.78 4.1,6.74a48.92,48.92 0,0 1,2.78 8.59c1.33,6.43 -1.94,13.48 -3.61,19.76 -1.79,6.74 -3.53,13.49 -5.44,20.2 -0.9,3.17 -1.83,6.33 -2.83,9.47 -0.45,1.43 -0.76,3.75 -1.65,5 0,1.24 -0.39,1.58 -1.24,1l-2.39,-0.77c-6.61,-2.11 -13.24,-4.18 -19.85,-6.31 -2.15,-0.7 -3.07,2.68 -0.93,3.37 4.4,1.42 8.8,2.81 13.2,4.2 3.06,1 7.86,3.63 11.07,3.38 2.36,-0.19 2.81,-2 3.51,-4 1.38,-3.9 2.58,-7.88 3.74,-11.86 2.71,-9.27 5,-18.65 7.57,-28 1,-3.63 2.83,-7.74 3.09,-11.48 0.2,-2.79 -1,-6.24 -1.84,-8.85 -1.43,-4.26 -3.33,-11.21 -7.51,-13.54 -3.42,-1.91 -8.09,-2.45 -11.81,-3.69 -4.7,-1.56 -9.35,-3.27 -14.06,-4.83 -2.73,-0.91 -6.59,-3 -9.58,-2.49 -2.5,0.46 -2.91,2.52 -3.61,4.66 -3.29,9.9 -6.37,19.88 -9.56,29.81 -0.68,2.15 2.69,3.07 3.38,0.93Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#0a0a0a"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m353.53,330.29c-0.71,-22.64 -21.79,-25.82 -35.68,-5.39 -4.41,6.49 -5.74,7.7 -6.26,5.71 -0.45,-1.71 1.8,-5.46 7.02,-11.66 4.43,-5.27 8.26,-11.36 9.06,-14.38l1.38,-5.23 0.14,4.78 0.14,4.78 7.3,-0.61c12.08,-1 19.85,9.75 17.68,24.46 -0.4,2.71 -0.64,1.96 -0.78,-2.45z"
|
android:pathData="M132.76,56.37a34.42,34.42 0,0 0,-3.06 9.42c-0.36,2.2 3,3.15 3.38,0.93a31.17,31.17 0,0 1,2.7 -8.58c1,-2 -2,-3.79 -3,-1.77Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#0a0a0a"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m213.33,370.85c0,-2.97 11.8,-10.84 16.28,-10.86 0.95,-0 3.37,-0.87 5.39,-1.92 2.02,-1.05 3.67,-1.35 3.67,-0.66 0,0.69 3.32,1.25 7.38,1.25 6.51,0 11.03,2.42 8.46,4.52 -1.42,1.16 -18.37,-0.98 -17.61,-2.22 0.55,-0.9 -0.28,-1.08 -2.35,-0.53 -1.76,0.47 -4.11,0.94 -5.21,1.03 -1.1,0.1 -0.2,0.63 2,1.19l4,1.02 -4.33,0.36c-2.38,0.2 -5.08,0.42 -6,0.49 -0.92,0.07 -1.97,0.46 -2.33,0.86 -2.17,2.38 -9.33,6.57 -9.33,5.46z"
|
android:pathData="M139.22,57.27a21,21 0,0 0,-4 9.44c-0.35,2.21 3,3.16 3.37,0.93A18.93,18.93 0,0 1,142.24 59c1.35,-1.8 -1.69,-3.55 -3,-1.76Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="?colorPrimary"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m457.6,571.97c-23.02,-6.03 -32.58,-14.58 -33.34,-29.82l-0.47,-9.48 3.43,4.91c26.73,38.3 87.45,27.04 95.19,-17.65l1.03,-5.93 1.6,6c9.25,34.78 -26.84,62.6 -67.43,51.97z"
|
android:pathData="M144,59.9l-2.4,8.66a1.75,1.75 0,0 0,3.38 0.93l2.4,-8.65A1.76,1.76 0,0 0,144 59.9Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="?colorPrimary"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m374.67,680.77c0,-9.44 -9.93,-90.09 -12.78,-103.85 -2.29,-11.06 -2.3,-10.25 0.11,-10.25 2.63,0 2.67,-7.81 0.07,-14.04 -1.56,-3.73 -2.66,-4.62 -5.71,-4.62 -4.28,0 -3.86,0.86 -15.13,-30.96 -7.45,-21.03 -11.35,-35.59 -12.62,-47.13l-0.89,-8.1 6.07,-2.32c5.77,-2.2 7.03,-4.89 3.51,-7.49 -3.2,-2.36 -2.52,-10.27 1.19,-13.82l3.83,-3.67 2.8,4.75c4.12,6.98 7.41,4.79 15.53,-10.34 3.14,-5.85 3.34,-5.88 8.59,-1.47l4.09,3.44 1.11,-3.11c0.61,-1.71 2.23,-8.14 3.6,-14.28 2.77,-12.45 4.38,-11.81 4.56,1.84 0.06,4.4 1.01,16.1 2.12,26l2.02,18 -4.78,38.59c-2.63,21.23 -4.5,39.68 -4.15,41.01 0.35,1.33 -0.02,4.14 -0.81,6.24 -1.7,4.5 0.61,11.6 4.16,12.81 1.2,0.41 3.1,2.68 4.22,5.04 2.64,5.61 57.58,108.35 61.35,114.74l2.89,4.9h-37.47c-31.53,0 -37.47,-0.3 -37.47,-1.9z"
|
android:pathData="M148.93,62.23a25.59,25.59 0,0 1,-0.94 7.83,1.76 1.76,0 0,0 3.38,0.94 28.9,28.9 0,0 0,1.06 -8.77c-0.07,-2.25 -3.57,-2.26 -3.5,0Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="?colorPrimary"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m313.95,678.33c0.99,-5.78 15.38,-102.42 15.38,-103.28 0,-0.69 -4.7,0.28 -8.54,1.75 -1.63,0.63 -2.13,-0.07 -2.17,-2.99 -0.03,-2.1 -0.88,-5.31 -1.9,-7.14 -1.68,-3.02 -1.59,-3.47 0.91,-4.82 2.59,-1.4 2.33,-1.81 -4.44,-7.11 -16.17,-12.66 -48.89,-48.63 -44.93,-49.39l3.6,-0.68 -10.15,-16c-9.14,-14.41 -19.05,-31.02 -19.05,-31.94 0,-0.19 1.5,-0.07 3.34,0.29 1.84,0.35 5.05,-0.07 7.13,-0.93 5.04,-2.09 13.89,2.63 13.31,7.09 -0.54,4.2 1.24,6.16 5.63,6.16 2.16,0 3.85,0.45 3.76,1 -0.64,4.08 0.35,4.55 8.49,4.03 4.58,-0.29 11.16,-1.36 14.61,-2.37 10.14,-2.98 12.86,1.48 31.5,51.55l10.5,28.21 -3.3,1.51c-6.95,3.17 -1.03,19.28 6.79,18.5l4.24,-0.42 2.9,17.33c1.6,9.53 4.77,33.23 7.04,52.67 2.28,19.43 4.43,36.68 4.79,38.33l0.65,3H338.63,313.21Z"
|
android:pathData="M155.87,63l-2.58,9.22a1.75,1.75 0,0 0,3.38 0.93L159.24,64a1.75,1.75 0,0 0,-3.37 -0.93Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="?colorPrimary"
|
android:fillColor="?colorOnSurface"
|
||||||
android:pathData="m146.58,677.69c-8.21,-9.35 -13.75,-23.51 -14.23,-36.36l-0.42,-11.33 4.42,11.39c7.61,19.63 14.84,25.5 34.63,28.15 22.05,2.95 53.63,-11.8 58.98,-27.54l1.81,-5.33 0.89,4c0.49,2.2 1.16,4.84 1.49,5.87 2.31,7.29 -9.43,26.59 -19.91,32.73 -5.74,3.37 -6.14,3.4 -34.55,3.4h-28.74z"
|
android:pathData="M161.44,64.86a64.54,64.54 0,0 1,-2.72 9.35c-0.75,2.13 2.63,3 3.38,0.93a65.83,65.83 0,0 0,2.72 -9.35c0.42,-2.2 -2.95,-3.14 -3.38,-0.93Z" />
|
||||||
android:strokeWidth="1.33333" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="m336.58,349.6c-6.62,-1.18 -9.81,-2.97 -12.49,-7 -2.18,-3.28 -3.24,-6.74 -4.11,-13.5 -0.3,-2.35 -0.59,-4.32 -0.63,-4.36 -0.11,-0.12 0,-0.28 -2.07,3.03 -2.21,3.55 -2.61,4.61 -2.59,6.82l0.02,1.64 -1.77,-1.62c-1.92,-1.75 -2.22,-2.3 -2.22,-4 0,-2.65 2.25,-6.8 6.2,-11.44 5.52,-6.48 8.34,-10.86 9.71,-15.09 0.54,-1.66 1.76,-4.07 1.96,-3.87 0.25,0.25 -1.3,5.17 -2.24,7.1 -1.43,2.95 -4.31,7.16 -8.18,11.97 -5.61,6.99 -7.22,9.82 -6.58,11.64 0.54,1.54 1.91,0.39 5.42,-4.59 2.09,-2.96 4.2,-5.71 4.29,-5.61 0.03,0.03 -0.08,0.73 -0.24,1.55 -0.16,0.82 -0.29,2.71 -0.3,4.2 -0.02,5.91 1.69,11.81 4.61,15.92 1.64,2.31 5.17,5.25 6.66,5.55 0.6,0.12 0.72,0.06 0.94,-0.41 0.2,-0.44 0.18,-0.83 -0.1,-1.94 -0.44,-1.77 -0.43,-2.38 0.06,-3.46 0.5,-1.1 1.4,-1.75 2.85,-2.05 2.24,-0.47 4.7,0.79 6.37,3.25 0.98,1.45 1.67,1.78 2.38,1.14 0.6,-0.54 0.99,-1.34 1.36,-2.8l0.32,-1.23 -0.12,1.33c-0.22,2.47 -0.61,3.36 -2.15,4.83 -1.72,1.64 -2.49,2.06 -3.09,1.68 -0.59,-0.37 -0.79,-0.18 -0.95,0.93 -0.08,0.53 -0.2,0.95 -0.28,0.94 -0.07,-0.01 -1.43,-0.25 -3.02,-0.54z"
|
|
||||||
android:strokeWidth="0.21377" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="m302.04,437.85c-3.5,-0.82 -6.47,-2.29 -9.34,-4.6 -2.03,-1.64 -5.08,-5.3 -4.72,-5.66 0.37,-0.37 1.49,0.16 2.39,1.12 1.09,1.16 4.2,3.28 6.56,4.47 3.37,1.69 5.64,2.19 10,2.2 4.26,0.01 6.13,-0.39 8.32,-1.78 1.24,-0.79 2.26,-1.01 2.54,-0.55 0.15,0.25 -1.93,1.93 -3.65,2.96 -3.05,1.82 -8.61,2.67 -12.1,1.85z"
|
|
||||||
android:strokeWidth="0.271863" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="m349.04,344.35c0.14,-0.18 0.57,-0.67 0.95,-1.1 0.79,-0.9 1.1,-1.47 1.49,-2.71 0.56,-1.79 0.69,-3 0.68,-6.73 -0,-2.62 -0.05,-3.8 -0.2,-5.02 -0.8,-6.45 -2.55,-11.67 -4.56,-13.59 -1.09,-1.04 -3.77,-1.97 -5.71,-1.97 -1.2,0 -1.94,0.22 -2.37,0.72 -0.71,0.81 -0.38,1.79 1.06,3.18 1.55,1.49 2.55,3.2 3.55,6.08 1.16,3.31 2.14,8.33 2.27,11.58 0.05,1.16 0.04,1.18 -0.06,0.39 -0.06,-0.45 -0.23,-1.5 -0.39,-2.32 -1.69,-8.93 -5.34,-15.78 -9.92,-18.64 -0.98,-0.61 -2.83,-1.33 -3.43,-1.34 -0.79,-0.01 -0.6,-0.19 0.59,-0.59 2.12,-0.7 3.14,-0.87 5.31,-0.89 1.25,-0.01 2.2,0.04 2.7,0.14 5.47,1.08 9.46,5.11 11.35,11.49 0.64,2.14 0.91,4.08 1.12,7.85 0.15,2.82 0.26,3.6 0.48,3.47 0.05,-0.03 0.18,-0.5 0.27,-1.03 1.14,-6.38 0.16,-13.01 -2.56,-17.48 -0.87,-1.43 -1.55,-2.3 -2.61,-3.34 -2.7,-2.66 -6.1,-4.11 -10.05,-4.31 -0.98,-0.05 -2.33,0.02 -5.43,0.28 -2.27,0.19 -4.14,0.33 -4.17,0.3 -0.08,-0.08 -0.29,-8.79 -0.22,-8.87 0.04,-0.04 0.22,0.07 0.4,0.24 0.57,0.54 0.84,1.7 0.98,4.12 0.1,1.76 0.23,2.11 0.95,2.49 0.4,0.21 0.62,0.23 2.54,0.23 1.15,-0 3.01,-0.02 4.14,-0.04 1.58,-0.03 2.29,0 3.14,0.15 8.74,1.52 14.16,9.98 14.18,22.14 0,4.04 -0.66,8.68 -1.66,11.5 -0.55,1.56 -0.79,1.9 -1.49,2.13 -0.67,0.22 -1.53,0.65 -2.65,1.31 -0.46,0.27 -0.87,0.5 -0.9,0.5 -0.03,0 0.06,-0.14 0.2,-0.32z"
|
|
||||||
android:strokeWidth="0.11033" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="m213.11,372.21c-0.39,-0.39 -0.28,-1.82 0.25,-3.35 0.3,-0.86 0.6,-1.29 1.59,-2.24 1.67,-1.6 4.54,-3.47 7.95,-5.15 3.3,-1.63 5.43,-2.38 7.56,-2.66 1.89,-0.25 3.28,-0.88 4.8,-2.17 0.64,-0.54 1.33,-1.07 1.54,-1.18 0.55,-0.29 1.22,-0.25 1.49,0.08 0.68,0.82 3.48,1.62 7.64,2.2 6.23,0.86 7.5,1.18 8.64,2.18 0.76,0.67 1.56,2.29 2.06,4.2 0.58,2.19 0.68,2.17 -3.67,0.86 -1.95,-0.58 -4.44,-0.82 -8.6,-0.82 -6.52,0 -13.55,0.8 -17.92,2.05 -2.39,0.68 -7.38,3.2 -9.86,4.98 -1.78,1.28 -2.9,1.61 -3.48,1.03zM215.76,370.3c2.05,-1.05 4.72,-2.81 6.26,-4.13 0.68,-0.59 1.52,-1.16 1.86,-1.28 0.46,-0.16 9.09,-1 11.39,-1.1 0.49,-0.02 -0.39,-0.32 -2.94,-0.98 -2.62,-0.68 -3.59,-1.05 -3.11,-1.2 0.15,-0.04 0.87,-0.17 1.6,-0.29 0.73,-0.11 2.19,-0.41 3.24,-0.66 1.05,-0.25 2.13,-0.43 2.4,-0.4 0.43,0.05 0.49,0.13 0.44,0.55 -0.05,0.42 0.04,0.53 0.61,0.77 2.33,0.97 12,2.21 15.43,1.98 1.21,-0.08 1.49,-0.16 1.87,-0.55 0.61,-0.61 0.62,-1.53 0.02,-2.16 -0.59,-0.63 -2.31,-1.41 -3.91,-1.77 -0.78,-0.18 -2.77,-0.37 -4.8,-0.46 -5.06,-0.23 -6.93,-0.56 -7.31,-1.26 -0.32,-0.6 -1.69,-0.37 -3.99,0.69 -2.73,1.26 -3.83,1.64 -5.4,1.85 -1.76,0.24 -3.04,0.69 -5.45,1.88 -3.47,1.72 -8.39,5.27 -9.72,7.02 -0.58,0.76 -1.05,1.66 -1.05,2.02 0,0.5 0.94,0.31 2.56,-0.52z"
|
|
||||||
android:strokeWidth="0.177896" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,81 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
|
|
||||||
android:id="@+id/checkableGroup"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:weightSum="3">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/button_list"
|
|
||||||
style="@style/Widget.Kotatsu.ToggleButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:text="@string/list"
|
|
||||||
app:icon="@drawable/ic_list" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/button_list_detailed"
|
|
||||||
style="@style/Widget.Kotatsu.ToggleButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="12dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:text="@string/detailed_list"
|
|
||||||
app:icon="@drawable/ic_list_detailed" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/button_grid"
|
|
||||||
style="@style/Widget.Kotatsu.ToggleButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:text="@string/grid"
|
|
||||||
app:icon="@drawable/ic_grid" />
|
|
||||||
|
|
||||||
</org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView_grid_title"
|
|
||||||
style="?materialAlertDialogTitleTextStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="?attr/dialogPreferredPadding"
|
|
||||||
android:paddingRight="?attr/dialogPreferredPadding"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/grid_size"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/slider_grid"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:stepSize="5"
|
|
||||||
android:valueFrom="50"
|
|
||||||
android:valueTo="150"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:labelBehavior="floating"
|
|
||||||
app:tickVisible="false"
|
|
||||||
tools:value="100"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user