diff --git a/.weblate b/.weblate new file mode 100644 index 000000000..4a120e1a3 --- /dev/null +++ b/.weblate @@ -0,0 +1,3 @@ +[weblate] +url = https://hosted.weblate.org/api/ +translation = kotatsu/strings diff --git a/app/build.gradle b/app/build.gradle index 9fea635b3..01c02c0c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 33 - versionCode 548 - versionName '5.1.4' + versionCode 551 + versionName '5.2-b1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -42,6 +42,7 @@ android { } sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + main.java.srcDirs += 'src/main/kotlin/' } compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -59,7 +60,7 @@ android { } lint { abortOnError true - disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged' + disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled' } testOptions { unitTests.includeAndroidResources true @@ -82,13 +83,13 @@ dependencies { exclude group: 'org.json', module: 'json' } - implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.activity:activity-ktx:1.7.2' - implementation 'androidx.fragment:fragment-ktx:1.5.7' + implementation 'androidx.fragment:fragment-ktx:1.6.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-service:2.6.1' @@ -105,7 +106,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.8.1' //noinspection GradleDependency - implementation('com.google.guava:guava:31.1-android') { + implementation('com.google.guava:guava:32.0.0-android') { exclude group: 'com.google.guava', module: 'failureaccess' exclude group: 'org.checkerframework', module: 'checker-qual' exclude group: 'com.google.j2objc', module: 'j2objc-annotations' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 791a045d4..a81d9ac31 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -8,7 +8,7 @@ public static void checkParameterIsNotNull(...); public static void checkNotNullParameter(...); } --keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment +-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment -keep class org.koitharu.kotatsu.core.db.entity.* { *; } -dontwarn okhttp3.internal.platform.** -dontwarn org.conscrypt.** diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt index 4b1784bed..6b9de214f 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt @@ -8,7 +8,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import javax.inject.Inject import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -19,7 +18,8 @@ import org.junit.runner.RunWith import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.awaitForIdle import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.data.HistoryRepository +import javax.inject.Inject @HiltAndroidTest @RunWith(AndroidJUnit4::class) diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt index f885ddb36..9984821cc 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt @@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.data.HistoryRepository import java.io.File import javax.inject.Inject diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt index 37b5ebf02..5e38b4d15 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.tracker.domain import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import javax.inject.Inject import junit.framework.TestCase.* import kotlinx.coroutines.test.runTest import org.junit.Before @@ -11,8 +10,9 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.koitharu.kotatsu.SampleData -import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.parsers.model.Manga +import javax.inject.Inject @HiltAndroidTest @RunWith(AndroidJUnit4::class) diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/util/LoggingAdapterDataObserver.kt b/app/src/debug/java/org/koitharu/kotatsu/core/util/LoggingAdapterDataObserver.kt new file mode 100644 index 000000000..2d47a63b3 --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/core/util/LoggingAdapterDataObserver.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.core.util + +import android.util.Log +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver + +class LoggingAdapterDataObserver( + private val tag: String, +) : AdapterDataObserver() { + + override fun onChanged() { + Log.d(tag, "onChanged()") + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)") + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)") + } + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)") + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)") + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)") + } + + override fun onStateRestorationPolicyChanged() { + Log.d(tag, "onStateRestorationPolicyChanged()") + } +} diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/util/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/core/util/ext/DebugExt.kt new file mode 100644 index 000000000..62af20cbc --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/core/util/ext/DebugExt.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.core.util.ext + +fun Throwable.printStackTraceDebug() = printStackTrace() diff --git a/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt deleted file mode 100644 index e00bb6a83..000000000 --- a/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -fun Throwable.printStackTraceDebug() = printStackTrace() \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 10faa7ae2..2a167d459 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,6 +102,10 @@ android:name="org.koitharu.kotatsu.browser.BrowserActivity" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:windowSoftInputMode="adjustResize" /> + : - Fragment(), - WindowInsetsDelegate.WindowInsetsListener { - - private var viewBinding: B? = null - - protected val binding: B - get() = checkNotNull(viewBinding) - - @JvmField - protected val exceptionResolver = ExceptionResolver(this) - - @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) - - protected val actionModeDelegate: ActionModeDelegate - get() = (requireActivity() as BaseActivity<*>).actionModeDelegate - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val binding = onInflateView(inflater, container) - viewBinding = binding - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - insetsDelegate.onViewCreated(view) - } - - override fun onDestroyView() { - viewBinding = null - insetsDelegate.onDestroyView() - super.onDestroyView() - } - - protected fun bindingOrNull() = viewBinding - - protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B -} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt deleted file mode 100644 index e43ca8877..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.koitharu.kotatsu.base.ui - -import android.graphics.Color -import android.os.Build -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import androidx.viewbinding.ViewBinding - -@Suppress("DEPRECATION") -private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - -@Suppress("DEPRECATION") -private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_FULLSCREEN or - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - -abstract class BaseFullscreenActivity : - BaseActivity(), - View.OnSystemUiVisibilityChangeListener { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - with(window) { - statusBarColor = Color.TRANSPARENT - navigationBarColor = Color.TRANSPARENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - attributes.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } - decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity) - } - showSystemUI() - } - - @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith") - @Deprecated("Deprecated in Java") - final override fun onSystemUiVisibilityChange(visibility: Int) { - onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) - } - - // TODO WindowInsetsControllerCompat works incorrect - @Suppress("DEPRECATION") - protected fun hideSystemUI() { - window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN - } - - @Suppress("DEPRECATION") - protected fun showSystemUI() { - window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN - } - - protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseService.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseService.kt deleted file mode 100644 index 05e07729e..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseService.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.koitharu.kotatsu.base.ui - -import androidx.lifecycle.LifecycleService - -abstract class BaseService : LifecycleService() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ScrollListenerInvalidationObserver.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ScrollListenerInvalidationObserver.kt deleted file mode 100644 index b3e30c910..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ScrollListenerInvalidationObserver.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.koitharu.kotatsu.base.ui.list - -import androidx.recyclerview.widget.RecyclerView - -class ScrollListenerInvalidationObserver( - private val recyclerView: RecyclerView, - private val scrollListener: RecyclerView.OnScrollListener, -) : RecyclerView.AdapterDataObserver() { - - override fun onChanged() { - super.onChanged() - invalidateScroll() - } - - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - super.onItemRangeInserted(positionStart, itemCount) - invalidateScroll() - } - - override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { - super.onItemRangeRemoved(positionStart, itemCount) - invalidateScroll() - } - - private fun invalidateScroll() { - recyclerView.post { - scrollListener.onScrolled(recyclerView, 0, 0) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt deleted file mode 100644 index d654e541d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CountedBooleanLiveData.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.koitharu.kotatsu.base.ui.util - -import androidx.annotation.AnyThread -import androidx.lifecycle.LiveData -import java.util.concurrent.atomic.AtomicInteger - -class CountedBooleanLiveData : LiveData(false) { - - private val counter = AtomicInteger(0) - - @AnyThread - fun increment() { - if (counter.getAndIncrement() == 0) { - postValue(true) - } - } - - @AnyThread - fun decrement() { - if (counter.decrementAndGet() == 0) { - postValue(false) - } - } - - @AnyThread - fun reset() { - if (counter.getAndSet(0) != 0) { - postValue(false) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt deleted file mode 100644 index 34f02003c..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt +++ /dev/null @@ -1,126 +0,0 @@ -package org.koitharu.kotatsu.browser.cloudflare - -import android.annotation.SuppressLint -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.webkit.CookieManager -import android.webkit.WebSettings -import androidx.appcompat.app.AlertDialog -import androidx.core.view.isInvisible -import androidx.fragment.app.setFragmentResult -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import okhttp3.Headers -import org.koitharu.kotatsu.base.ui.AlertDialogFragment -import org.koitharu.kotatsu.browser.WebViewBackPressedCallback -import org.koitharu.kotatsu.core.network.CommonHeaders -import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor -import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar -import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding -import org.koitharu.kotatsu.utils.ext.withArgs -import javax.inject.Inject - -@AndroidEntryPoint -class CloudFlareDialog : AlertDialogFragment(), CloudFlareCallback { - - private lateinit var url: String - private val pendingResult = Bundle(1) - - @Inject - lateinit var cookieJar: MutableCookieJar - - private var onBackPressedCallback: WebViewBackPressedCallback? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - url = requireArguments().getString(ARG_URL).orEmpty() - } - - override fun onInflateView( - inflater: LayoutInflater, - container: ViewGroup?, - ) = FragmentCloudflareBinding.inflate(inflater, container, false) - - @SuppressLint("SetJavaScriptEnabled") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(binding.webView.settings) { - javaScriptEnabled = true - cacheMode = WebSettings.LOAD_DEFAULT - domStorageEnabled = true - databaseEnabled = true - userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome - } - binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url) - CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true) - if (url.isEmpty()) { - dismissAllowingStateLoss() - } else { - binding.webView.loadUrl(url) - } - } - - override fun onDestroyView() { - binding.webView.stopLoading() - binding.webView.destroy() - onBackPressedCallback = null - super.onDestroyView() - } - - override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { - return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null) - } - - override fun onDialogCreated(dialog: AlertDialog) { - super.onDialogCreated(dialog) - onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also { - dialog.onBackPressedDispatcher.addCallback(it) - } - } - - override fun onResume() { - super.onResume() - binding.webView.onResume() - } - - override fun onPause() { - binding.webView.onPause() - super.onPause() - } - - override fun onDismiss(dialog: DialogInterface) { - setFragmentResult(TAG, pendingResult) - super.onDismiss(dialog) - } - - override fun onPageLoaded() { - bindingOrNull()?.progressBar?.isInvisible = true - } - - override fun onCheckPassed() { - pendingResult.putBoolean(EXTRA_RESULT, true) - dismissAllowingStateLoss() - } - - override fun onHistoryChanged() { - onBackPressedCallback?.onHistoryChanged() - } - - companion object { - - const val TAG = "CloudFlareDialog" - const val EXTRA_RESULT = "result" - private const val ARG_URL = "url" - private const val ARG_UA = "ua" - - fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) { - putString(ARG_URL, url) - headers?.get(CommonHeaders.USER_AGENT)?.let { - putString(ARG_UA, it) - } - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt deleted file mode 100644 index 7168445bd..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.koitharu.kotatsu.core.parser - -import android.content.Context -import androidx.annotation.ColorRes -import dagger.Reusable -import dagger.hilt.android.qualifiers.ApplicationContext -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.parsers.model.MangaTag -import javax.inject.Inject - -@Reusable -class MangaTagHighlighter @Inject constructor( - @ApplicationContext context: Context, -) { - - private val dict by lazy { - context.resources.openRawResource(R.raw.tags_redlist).use { - val set = HashSet() - it.bufferedReader().forEachLine { x -> - val line = x.trim() - if (line.isNotEmpty()) { - set.add(line) - } - } - set - } - } - - @ColorRes - fun getTint(tag: MangaTag): Int { - return if (tag.title.lowercase() in dict) { - R.color.warning - } else { - 0 - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt deleted file mode 100644 index b0072f4a8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ /dev/null @@ -1,162 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.scopes.ViewModelScoped -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.model.getPreferredBranch -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.toListItem -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.exception.NotFoundException -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import javax.inject.Inject - -@ViewModelScoped -class MangaDetailsDelegate @Inject constructor( - savedStateHandle: SavedStateHandle, - private val mangaDataRepository: MangaDataRepository, - private val historyRepository: HistoryRepository, - private val localMangaRepository: LocalMangaRepository, - private val mangaRepositoryFactory: MangaRepository.Factory, -) { - private val intent = MangaIntent(savedStateHandle) - private val mangaData = MutableStateFlow(intent.manga) - - val selectedBranch = MutableStateFlow(null) - - // Remote manga for saved and saved for remote - val relatedManga = MutableStateFlow(null) - val manga: StateFlow - get() = mangaData - val mangaId = intent.manga?.id ?: intent.mangaId - - suspend fun doLoad() { - var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") - mangaData.value = manga - manga = mangaRepositoryFactory.create(manga.source).getDetails(manga) - // find default branch - val hist = historyRepository.getOne(manga) - selectedBranch.value = manga.getPreferredBranch(hist) - mangaData.value = manga - relatedManga.value = runCatchingCancellable { - if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null - mangaRepositoryFactory.create(m.source).getDetails(m) - } else { - localMangaRepository.findSavedManga(manga)?.manga - } - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrNull() - } - - fun mapChapters( - manga: Manga?, - related: Manga?, - history: MangaHistory?, - newCount: Int, - branch: String?, - ): List { - val chapters = manga?.chapters ?: return emptyList() - val relatedChapters = related?.chapters - return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) { - mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch) - } else { - mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch) - } - } - - private fun mapChapters( - chapters: List, - downloadedChapters: List?, - currentId: Long?, - newCount: Int, - branch: String?, - ): List { - val result = ArrayList(chapters.size) - val currentIndex = chapters.indexOfFirst { it.id == currentId } - val firstNewIndex = chapters.size - newCount - val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id } - for (i in chapters.indices) { - val chapter = chapters[i] - if (chapter.branch != branch) { - continue - } - result += chapter.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = false, - isDownloaded = downloadedIds?.contains(chapter.id) == true, - ) - } - if (result.size < chapters.size / 2) { - result.trimToSize() - } - return result - } - - private fun mapChaptersWithSource( - chapters: List, - sourceChapters: List, - currentId: Long?, - newCount: Int, - branch: String?, - ): List { - val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id } - val result = ArrayList(sourceChapters.size) - val currentIndex = sourceChapters.indexOfFirst { it.id == currentId } - val firstNewIndex = sourceChapters.size - newCount - for (i in sourceChapters.indices) { - val chapter = sourceChapters[i] - val localChapter = chaptersMap.remove(chapter.id) - if (chapter.branch != branch) { - continue - } - result += localChapter?.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = false, - isDownloaded = false, - ) ?: chapter.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = true, - isDownloaded = false, - ) - } - if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source - result.ensureCapacity(result.size + chaptersMap.size) - chaptersMap.values.mapNotNullTo(result) { - if (it.branch == branch) { - it.toListItem( - isCurrent = false, - isUnread = true, - isNew = false, - isMissing = false, - isDownloaded = false, - ) - } else { - null - } - } - result.sortBy { it.chapter.number } - } - if (result.size < sourceChapters.size / 2) { - result.trimToSize() - } - return result - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt deleted file mode 100644 index ab051c176..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.koitharu.kotatsu.details.ui.adapter - -import androidx.core.view.isVisible -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.databinding.ItemChapterBinding -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.textAndVisible - -fun chapterListItemAD( - clickListener: OnListItemClickListener, -) = adapterDelegateViewBinding( - { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) } -) { - - val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener) - itemView.setOnClickListener(eventListener) - itemView.setOnLongClickListener(eventListener) - - bind { payloads -> - if (payloads.isEmpty()) { - binding.textViewTitle.text = item.chapter.name - binding.textViewNumber.text = item.chapter.number.toString() - binding.textViewDescription.textAndVisible = item.description() - } - when (item.status) { - FLAG_UNREAD -> { - binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default) - binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary)) - } - FLAG_CURRENT -> { - binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent) - binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) - } - else -> { - binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline) - binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary)) - } - } - val isMissing = item.hasFlag(FLAG_MISSING) - binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f - binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f - binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f - - binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED) - binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt deleted file mode 100644 index df6e54ca0..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package org.koitharu.kotatsu.favourites.ui.categories.select.adapter - -import androidx.recyclerview.widget.DiffUtil -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem - -class MangaCategoriesAdapter( - clickListener: OnListItemClickListener -) : AsyncListDifferDelegationAdapter(DiffCallback()) { - - init { - delegatesManager.addDelegate(mangaCategoryAD(clickListener)) - } - - private class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem - ): Boolean = oldItem.id == newItem.id - - override fun areContentsTheSame( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem - ): Boolean = oldItem == newItem - - override fun getChangePayload( - oldItem: MangaCategoryItem, - newItem: MangaCategoryItem - ): Any? { - if (oldItem.isChecked != newItem.isChecked) { - return newItem.isChecked - } - return super.getChangePayload(oldItem, newItem) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt deleted file mode 100644 index 7c547e0ae..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.list.domain - -interface ListExtraProvider { - - suspend fun getCounter(mangaId: Long): Int - - suspend fun getProgress(mangaId: Long): Float -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt deleted file mode 100644 index 6b1fe569f..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import androidx.recyclerview.widget.AsyncListDiffer.ListListener -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter - -class FilterAdapter( - listener: OnFilterChangedListener, - listListener: ListListener, -) : AsyncListDifferDelegationAdapter( - FilterDiffCallback(), - filterSortDelegate(listener), - filterTagDelegate(listener), - filterHeaderDelegate(), - filterLoadingDelegate(), - filterErrorDelegate(), -) { - - init { - differ.addListListener(listListener) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt deleted file mode 100644 index e34b5ad94..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import android.widget.TextView -import androidx.core.view.isVisible -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.titleRes -import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding -import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding - -fun filterSortDelegate( - listener: OnFilterChangedListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) } -) { - - itemView.setOnClickListener { - listener.onSortItemClick(item) - } - - bind { - binding.root.setText(item.order.titleRes) - binding.root.isChecked = item.isSelected - } -} - -fun filterTagDelegate( - listener: OnFilterChangedListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) } -) { - - itemView.setOnClickListener { - listener.onTagItemClick(item) - } - - bind { - binding.root.text = item.tag.title - binding.root.isChecked = item.isChecked - } -} - -fun filterHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } -) { - - bind { - binding.textViewTitle.setText(item.titleResId) - binding.badge.isVisible = if (item.counter == 0) { - false - } else { - binding.badge.text = item.counter.toString() - true - } - } -} - -fun filterLoadingDelegate() = adapterDelegate(R.layout.item_loading_footer) {} - -fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { - - bind { - (itemView as TextView).setText(item.textResId) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt deleted file mode 100644 index 226d73ed6..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.LinearLayoutManager -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet -import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback -import org.koitharu.kotatsu.databinding.SheetFilterBinding -import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel -import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels - -class FilterBottomSheet : - BaseBottomSheet(), - MenuItem.OnActionExpandListener, - SearchView.OnQueryTextListener, - AsyncListDiffer.ListListener { - - private val viewModel by parentFragmentViewModels() - private var collapsibleActionViewCallback: CollapseActionViewCallback? = null - - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { - return SheetFilterBinding.inflate(inflater, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val adapter = FilterAdapter(viewModel, this) - binding.recyclerView.adapter = adapter - viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) - initOptionsMenu() - } - - override fun onDestroyView() { - super.onDestroyView() - collapsibleActionViewCallback = null - } - - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - setExpanded(isExpanded = true, isLocked = true) - collapsibleActionViewCallback?.onMenuItemActionExpand(item) - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - val searchView = (item.actionView as? SearchView) ?: return false - searchView.setQuery("", false) - searchView.post { setExpanded(isExpanded = false, isLocked = false) } - collapsibleActionViewCallback?.onMenuItemActionCollapse(item) - return true - } - - override fun onQueryTextSubmit(query: String?): Boolean = false - - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.filterSearch(newText?.trim().orEmpty()) - return true - } - - override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { - if (currentList.size > previousList.size && view != null) { - (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) - } - } - - private fun initOptionsMenu() { - binding.headerBar.inflateMenu(R.menu.opt_filter) - val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also { - onBackPressedDispatcher.addCallback(it) - } - } - - companion object { - - private const val TAG = "FilterBottomSheet" - - fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt deleted file mode 100644 index 4549c46cf..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import androidx.recyclerview.widget.DiffUtil - -class FilterDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { - return when { - oldItem === newItem -> true - oldItem.javaClass != newItem.javaClass -> false - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.titleResId == newItem.titleResId - } - oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { - oldItem.tag == newItem.tag - } - oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { - oldItem.order == newItem.order - } - oldItem is FilterItem.Error && newItem is FilterItem.Error -> { - oldItem.textResId == newItem.textResId - } - else -> false - } - } - - override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean { - return when { - oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.counter == newItem.counter - } - oldItem is FilterItem.Error && newItem is FilterItem.Error -> true - oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { - oldItem.isChecked == newItem.isChecked - } - oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { - oldItem.isSelected == newItem.isSelected - } - else -> false - } - } - - override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? { - val hasPayload = when { - oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { - oldItem.isChecked != newItem.isChecked - } - oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { - oldItem.isSelected != newItem.isSelected - } - oldItem is FilterItem.Header && newItem is FilterItem.Header -> { - oldItem.counter != newItem.counter - } - else -> false - } - return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt deleted file mode 100644 index bbef939cb..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.koitharu.kotatsu.list.ui.filter - -import androidx.annotation.StringRes -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.SortOrder - -sealed interface FilterItem { - - class Header( - @StringRes val titleResId: Int, - val counter: Int, - ) : FilterItem - - class Sort( - val order: SortOrder, - val isSelected: Boolean, - ) : FilterItem - - class Tag( - val tag: MangaTag, - val isChecked: Boolean, - ) : FilterItem - - object Loading : FilterItem - - class Error( - @StringRes val textResId: Int, - ) : FilterItem -} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt deleted file mode 100644 index 7a754dcc6..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.list.ui.model - -object LoadingFooter : ListModel { - - override fun equals(other: Any?): Boolean = other === LoadingFooter -} diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt deleted file mode 100644 index d8c7b4967..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ /dev/null @@ -1,194 +0,0 @@ -package org.koitharu.kotatsu.local.ui - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asFlow -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.widgets.ChipsView -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import org.koitharu.kotatsu.list.domain.ListExtraProvider -import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.ListHeader2 -import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.list.ui.model.toUi -import org.koitharu.kotatsu.local.data.LocalManga -import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import java.io.IOException -import java.util.LinkedList -import javax.inject.Inject - -@HiltViewModel -class LocalListViewModel @Inject constructor( - private val repository: LocalMangaRepository, - private val historyRepository: HistoryRepository, - private val trackingRepository: TrackingRepository, - private val settings: AppSettings, - private val tagHighlighter: MangaTagHighlighter, - @LocalStorageChanges private val localStorageChanges: SharedFlow, - downloadScheduler: DownloadWorker.Scheduler, -) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { - - val onMangaRemoved = SingleLiveEvent() - val sortOrder = MutableLiveData(settings.localListOrder) - private val listError = MutableStateFlow(null) - private val mangaList = MutableStateFlow?>(null) - private val selectedTags = MutableStateFlow>(emptySet()) - private var refreshJob: Job? = null - - override val content = combine( - mangaList, - listModeFlow, - sortOrder.asFlow(), - selectedTags, - listError, - ) { list, mode, order, tags, error -> - when { - error != null -> listOf(error.toErrorState(canRetry = true)) - list == null -> listOf(LoadingState) - list.isEmpty() -> listOf( - EmptyState( - icon = R.drawable.ic_empty_local, - textPrimary = R.string.text_local_holder_primary, - textSecondary = R.string.text_local_holder_secondary, - actionStringRes = R.string._import, - ), - ) - - else -> buildList(list.size + 1) { - add(createHeader(list, tags, order)) - list.toUi(this, mode, this@LocalListViewModel, tagHighlighter) - } - } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) - - init { - onRefresh() - launchJob(Dispatchers.Default) { - localStorageChanges - .collectLatest { - if (refreshJob?.isActive != true) { - doRefresh() - } - } - } - } - - override fun onUpdateFilter(tags: Set) { - selectedTags.value = tags - onRefresh() - } - - override fun onRefresh() { - val prevJob = refreshJob - refreshJob = launchLoadingJob(Dispatchers.Default) { - prevJob?.cancelAndJoin() - doRefresh() - } - } - - override fun onRetry() = onRefresh() - - fun setSortOrder(value: SortOrder) { - sortOrder.value = value - settings.localListOrder = value - onRefresh() - } - - fun delete(ids: Set) { - launchLoadingJob(Dispatchers.Default) { - val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids } - for (manga in itemsToRemove) { - val original = repository.getRemoteManga(manga) - repository.delete(manga) || throw IOException("Unable to delete file") - runCatchingCancellable { - historyRepository.deleteOrSwap(manga, original) - } - mangaList.update { list -> - list?.filterNot { it.id == manga.id } - } - } - onMangaRemoved.emitCall(Unit) - } - } - - private suspend fun doRefresh() { - try { - listError.value = null - mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - listError.value = e - } - } - - private fun createHeader(mangaList: List, selectedTags: Set, order: SortOrder): ListHeader2 { - val tags = HashMap() - for (item in mangaList) { - for (tag in item.tags) { - tags[tag] = tags[tag]?.plus(1) ?: 1 - } - } - val topTags = tags.entries.sortedByDescending { it.value }.take(6) - val chips = LinkedList() - for ((tag, _) in topTags) { - val model = ChipsView.ChipModel( - tint = 0, - title = tag.title, - isCheckable = true, - isChecked = tag in selectedTags, - data = tag, - ) - if (model.isChecked) { - chips.addFirst(model) - } else { - chips.addLast(model) - } - } - return ListHeader2( - chips = chips, - sortOrder = order, - hasSelectedTags = selectedTags.isNotEmpty(), - ) - } - - override suspend fun getCounter(mangaId: Long): Int { - return if (settings.isTrackerEnabled) { - trackingRepository.getNewChaptersCount(mangaId) - } else { - 0 - } - } - - override suspend fun getProgress(mangaId: Long): Float { - return if (settings.isReadingIndicatorsEnabled) { - historyRepository.getProgress(mangaId) - } else { - PROGRESS_NONE - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt deleted file mode 100644 index 78c4f29a2..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.main.ui.owners - -import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar - -interface NoModalBottomSheetOwner { - - val bsHeader: BottomSheetHeaderBar? -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt deleted file mode 100644 index d5b3e517f..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.colorfilter - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.domain.ReaderColorFilter -import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.emitValue -import javax.inject.Inject - -@HiltViewModel -class ColorFilterConfigViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val mangaRepositoryFactory: MangaRepository.Factory, - private val mangaDataRepository: MangaDataRepository, -) : BaseViewModel() { - - private val manga = checkNotNull(savedStateHandle.get(EXTRA_MANGA)?.manga) - - private var initialColorFilter: ReaderColorFilter? = null - val colorFilter = MutableLiveData(null) - val onDismiss = SingleLiveEvent() - val preview = MutableLiveData(null) - - val isChanged: Boolean - get() = colorFilter.value != initialColorFilter - - init { - val page = checkNotNull( - savedStateHandle.get(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(), - ) - launchLoadingJob { - initialColorFilter = mangaDataRepository.getColorFilter(manga.id) - colorFilter.value = initialColorFilter - } - launchLoadingJob(Dispatchers.Default) { - val repository = mangaRepositoryFactory.create(page.source) - val url = repository.getPageUrl(page) - preview.emitValue( - MangaPage( - id = page.id, - url = url, - preview = page.preview, - source = page.source, - ), - ) - } - } - - fun setBrightness(brightness: Float) { - val cf = colorFilter.value - colorFilter.value = ReaderColorFilter(brightness, cf?.contrast ?: 0f).takeUnless { it.isEmpty } - } - - fun setContrast(contrast: Float) { - val cf = colorFilter.value - colorFilter.value = ReaderColorFilter(cf?.brightness ?: 0f, contrast).takeUnless { it.isEmpty } - } - - fun reset() { - colorFilter.value = null - } - - fun save() { - launchLoadingJob(Dispatchers.Default) { - mangaDataRepository.saveColorFilter(manga, colorFilter.value) - onDismiss.emitCall(Unit) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt deleted file mode 100644 index 38db30b5e..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import org.koitharu.kotatsu.parsers.model.MangaPage - -fun interface OnPageSelectListener { - - fun onPageSelected(page: MangaPage) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt deleted file mode 100644 index 22c5ddad5..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.parsers.model.MangaPage - -data class PageThumbnail( - val number: Int, - val isCurrent: Boolean, - val repository: MangaRepository, - val page: MangaPage -) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt deleted file mode 100644 index 0d9ca8b22..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ /dev/null @@ -1,146 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.GridLayoutManager -import coil.ImageLoader -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar -import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.databinding.SheetPagesBinding -import org.koitharu.kotatsu.list.ui.MangaListSpanResolver -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.domain.PageLoader -import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter -import org.koitharu.kotatsu.utils.ext.getParcelableCompat -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -import org.koitharu.kotatsu.utils.ext.withArgs -import javax.inject.Inject - -@AndroidEntryPoint -class PagesThumbnailsSheet : - BaseBottomSheet(), - OnListItemClickListener, - BottomSheetHeaderBar.OnExpansionChangeListener { - - @Inject - lateinit var mangaRepositoryFactory: MangaRepository.Factory - - @Inject - lateinit var pageLoader: PageLoader - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var settings: AppSettings - - private lateinit var thumbnails: List - private var spanResolver: MangaListSpanResolver? = null - private var currentPageIndex = -1 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pages = arguments?.getParcelableCompat(ARG_PAGES)?.pages - if (pages.isNullOrEmpty()) { - dismissAllowingStateLoss() - return - } - currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex) - val repository = mangaRepositoryFactory.create(pages.first().source) - thumbnails = pages.mapIndexed { i, x -> - PageThumbnail( - number = i + 1, - isCurrent = i == currentPageIndex, - repository = repository, - page = x, - ) - } - } - - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding { - return SheetPagesBinding.inflate(inflater, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - spanResolver = MangaListSpanResolver(view.resources) - with(binding.headerBar) { - title = arguments?.getString(ARG_TITLE) - subtitle = null - addOnExpansionChangeListener(this@PagesThumbnailsSheet) - } - - with(binding.recyclerView) { - addItemDecoration( - SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), - ) - adapter = PageThumbnailAdapter( - dataSet = thumbnails, - coil = coil, - scope = viewLifecycleScope, - loader = pageLoader, - clickListener = this@PagesThumbnailsSheet, - ) - addOnLayoutChangeListener(spanResolver) - spanResolver?.setGridSize(settings.gridSize / 100f, this) - if (currentPageIndex > 0) { - val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width) - (layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset) - } - } - } - - override fun onDestroyView() { - super.onDestroyView() - spanResolver = null - } - - override fun onItemClick(item: MangaPage, view: View) { - ( - (parentFragment as? OnPageSelectListener) - ?: (activity as? OnPageSelectListener) - )?.run { - onPageSelected(item) - dismiss() - } - } - - override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { - if (isExpanded) { - headerBar.subtitle = resources.getQuantityString( - R.plurals.pages, - thumbnails.size, - thumbnails.size, - ) - } else { - headerBar.subtitle = null - } - } - - companion object { - - private const val ARG_PAGES = "pages" - private const val ARG_TITLE = "title" - private const val ARG_CURRENT = "current" - - private const val TAG = "PagesThumbnailsSheet" - - fun show(fm: FragmentManager, pages: List, title: String, currentPage: Int) = - PagesThumbnailsSheet().withArgs(3) { - putParcelable(ARG_PAGES, ParcelableMangaPages(pages)) - putString(ARG_TITLE, title) - putInt(ARG_CURRENT, currentPage) - }.show(fm, TAG) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt deleted file mode 100644 index 7d2c6c3bd..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails.adapter - -import android.graphics.drawable.Drawable -import coil.ImageLoader -import coil.request.ImageRequest -import coil.size.Scale -import coil.size.Size -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.databinding.ItemPageThumbBinding -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.domain.PageLoader -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail -import org.koitharu.kotatsu.utils.ext.decodeRegion -import org.koitharu.kotatsu.utils.ext.isLowRamDevice -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.setTextColorAttr -import com.google.android.material.R as materialR - -fun pageThumbnailAD( - coil: ImageLoader, - scope: CoroutineScope, - loader: PageLoader, - clickListener: OnListItemClickListener, -) = adapterDelegateViewBinding( - { inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }, -) { - var job: Job? = null - val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) - val thumbSize = Size( - width = gridWidth, - height = (gridWidth / 13f * 18f).toInt(), - ) - - suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) { - item.page.preview?.let { url -> - coil.execute( - ImageRequest.Builder(context) - .data(url) - .tag(item.page.source) - .size(thumbSize) - .scale(Scale.FILL) - .allowRgb565(true) - .build(), - ).drawable - }?.let { drawable -> - return@withContext drawable - } - val file = loader.loadPage(item.page, force = false) - coil.execute( - ImageRequest.Builder(context) - .data(file) - .size(thumbSize) - .decodeRegion(0) - .allowRgb565(isLowRamDevice(context)) - .build(), - ).drawable - } - - binding.root.setOnClickListener { - clickListener.onItemClick(item.page, itemView) - } - - bind { - job?.cancel() - binding.imageViewThumb.setImageDrawable(null) - with(binding.textViewNumber) { - setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty) - setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary) - text = (item.number).toString() - } - job = scope.launch { - val drawable = runCatchingCancellable { - loadPageThumbnail(item) - }.getOrNull() - binding.imageViewThumb.setImageDrawable(drawable) - } - } - - onViewRecycled { - job?.cancel() - job = null - binding.imageViewThumb.setImageDrawable(null) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt deleted file mode 100644 index b293d2865..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.koitharu.kotatsu.reader.ui.thumbnails.adapter - -import coil.ImageLoader -import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter -import kotlinx.coroutines.CoroutineScope -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.domain.PageLoader -import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail - -class PageThumbnailAdapter( - dataSet: List, - coil: ImageLoader, - scope: CoroutineScope, - loader: PageLoader, - clickListener: OnListItemClickListener -) : ListDelegationAdapter>() { - - init { - delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener)) - setItems(dataSet) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt deleted file mode 100644 index c1063de8c..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.koitharu.kotatsu.search.ui - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.graphics.Insets -import androidx.core.view.updatePadding -import androidx.fragment.app.commit -import com.google.android.material.appbar.AppBarLayout -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags -import org.koitharu.kotatsu.databinding.ActivityContainerBinding -import org.koitharu.kotatsu.local.ui.LocalListFragment -import org.koitharu.kotatsu.main.ui.owners.AppBarOwner -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat -import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat - -@AndroidEntryPoint -class MangaListActivity : - BaseActivity(), - AppBarOwner { - - override val appBar: AppBarLayout - get() = binding.appbar - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityContainerBinding.inflate(layoutInflater)) - val tags = intent.getParcelableExtraCompat(EXTRA_TAGS)?.tags - supportActionBar?.setDisplayHomeAsUpEnabled(true) - val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source - if (source == null) { - finishAfterTransition() - return - } - title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title - val fm = supportFragmentManager - if (fm.findFragmentById(R.id.container) == null) { - fm.commit { - setReorderingAllowed(true) - val fragment = if (source == MangaSource.LOCAL) { - LocalListFragment.newInstance() - } else { - RemoteListFragment.newInstance(source) - } - replace(R.id.container, fragment) - if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { - runOnCommit(ApplyFilterRunnable(fragment, tags)) - } - } - } - } - - override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( - left = insets.left, - right = insets.right, - ) - } - - private class ApplyFilterRunnable( - private val fragment: RemoteListFragment, - private val tags: Set, - ) : Runnable { - - override fun run() { - fragment.viewModel.applyFilter(tags) - } - } - - companion object { - - private const val EXTRA_TAGS = "tags" - private const val EXTRA_SOURCE = "source" - - fun newIntent(context: Context, tags: Set) = Intent(context, MangaListActivity::class.java) - .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) - - fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java) - .putExtra(EXTRA_SOURCE, source) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt deleted file mode 100644 index ad02acf64..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.os.Bundle -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment - -class RootSettingsFragment : BasePreferenceFragment(R.string.settings) { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_root) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt deleted file mode 100644 index 53e1af007..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.fragment.app.commit -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceHeaderFragmentCompat -import androidx.slidingpanelayout.widget.SlidingPaneLayout -import org.koitharu.kotatsu.R - -class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener { - - private var currentTitle: CharSequence? = null - - override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - slidingPaneLayout.addPanelSlideListener(this) - } - - override fun onPanelSlide(panel: View, slideOffset: Float) = Unit - - override fun onPanelOpened(panel: View) { - activity?.title = currentTitle ?: getString(R.string.settings) - } - - override fun onPanelClosed(panel: View) { - activity?.setTitle(R.string.settings) - } - - fun setTitle(title: CharSequence?) { - currentTitle = title - if (slidingPaneLayout.width != 0 && slidingPaneLayout.isOpen) { - activity?.title = title - } - } - - fun openFragment(fragment: Fragment) { - childFragmentManager.commit { - setReorderingAllowed(true) - replace(androidx.preference.R.id.preferences_detail, fragment) - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - addToBackStack(null) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt deleted file mode 100644 index 43327d44a..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ /dev/null @@ -1,133 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import androidx.preference.Preference -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.RemoteMangaRepository -import org.koitharu.kotatsu.parsers.exception.AuthRequiredException -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity -import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.requireSerializable -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -import org.koitharu.kotatsu.utils.ext.withArgs -import javax.inject.Inject - -@AndroidEntryPoint -class SourceSettingsFragment : BasePreferenceFragment(0) { - - @Inject - lateinit var mangaRepositoryFactory: MangaRepository.Factory - - private lateinit var source: MangaSource - private var repository: RemoteMangaRepository? = null - private val exceptionResolver = ExceptionResolver(this) - - override fun onCreate(savedInstanceState: Bundle?) { - source = requireArguments().requireSerializable(EXTRA_SOURCE) - repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository - super.onCreate(savedInstanceState) - } - - override fun onResume() { - super.onResume() - setTitle(source.title) - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.sharedPreferencesName = source.name - val repo = repository ?: return - addPreferencesFromResource(R.xml.pref_source) - addPreferencesFromRepository(repo) - - findPreference(KEY_AUTH)?.run { - val authProvider = repo.getAuthProvider() - isVisible = authProvider != null - isEnabled = authProvider?.isAuthorized == false - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - findPreference(KEY_AUTH)?.run { - if (isVisible) { - loadUsername(viewLifecycleOwner, this) - } - } - } - - override fun onPreferenceTreeClick(preference: Preference): Boolean { - return when (preference.key) { - KEY_AUTH -> { - startActivity(SourceAuthActivity.newIntent(preference.context, source)) - true - } - - else -> super.onPreferenceTreeClick(preference) - } - } - - private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch { - runCatchingCancellable { - preference.summary = null - withContext(Dispatchers.Default) { - requireNotNull(repository?.getAuthProvider()?.getUsername()) - } - }.onSuccess { username -> - preference.title = getString(R.string.logged_in_as, username) - }.onFailure { error -> - when { - error is AuthRequiredException -> Unit - ExceptionResolver.canResolve(error) -> { - ensureActive() - Snackbar.make( - listView ?: return@onFailure, - error.getDisplayMessage(preference.context.resources), - Snackbar.LENGTH_INDEFINITE, - ).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } - .show() - } - - else -> preference.summary = error.getDisplayMessage(preference.context.resources) - } - error.printStackTraceDebug() - } - } - - private fun resolveError(error: Throwable) { - view ?: return - viewLifecycleScope.launch { - if (exceptionResolver.resolve(error)) { - val pref = findPreference(KEY_AUTH) ?: return@launch - val lifecycleOwner = awaitViewLifecycle() - loadUsername(lifecycleOwner, pref) - } - } - } - - companion object { - - private const val KEY_AUTH = "auth" - - private const val EXTRA_SOURCE = "source" - - fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) { - putSerializable(EXTRA_SOURCE, source) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt deleted file mode 100644 index 53ad51cbc..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.koitharu.kotatsu.settings.backup - -import android.content.ActivityNotFoundException -import android.net.Uri -import android.os.Bundle -import androidx.activity.result.ActivityResultCallback -import androidx.activity.result.contract.ActivityResultContracts -import androidx.preference.Preference -import com.google.android.material.snackbar.Snackbar -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug - -class BackupSettingsFragment : - BasePreferenceFragment(R.string.backup_restore), - ActivityResultCallback { - - private val backupSelectCall = registerForActivityResult( - ActivityResultContracts.OpenDocument(), - this - ) - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_backup) - } - - override fun onPreferenceTreeClick(preference: Preference): Boolean { - return when (preference.key) { - AppSettings.KEY_BACKUP -> { - BackupDialogFragment().show(childFragmentManager, BackupDialogFragment.TAG) - true - } - AppSettings.KEY_RESTORE -> { - try { - backupSelectCall.launch(arrayOf("*/*")) - } catch (e: ActivityNotFoundException) { - e.printStackTraceDebug() - Snackbar.make( - listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT - ).show() - } - true - } - else -> super.onPreferenceTreeClick(preference) - } - } - - override fun onActivityResult(result: Uri?) { - RestoreDialogFragment.newInstance(result ?: return) - .show(childFragmentManager, BackupDialogFragment.TAG) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt b/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt deleted file mode 100644 index 797893609..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.koitharu.kotatsu.utils - -import androidx.lifecycle.LiveData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -private const val DEFAULT_TIMEOUT = 5_000L - -/** - * Similar to a CoroutineLiveData but optimized for using within infinite flows - */ -class FlowLiveData( - private val flow: Flow, - defaultValue: T, - context: CoroutineContext = EmptyCoroutineContext, - private val timeoutInMs: Long = DEFAULT_TIMEOUT, -) : LiveData(defaultValue) { - - private val scope = CoroutineScope(Dispatchers.Main.immediate + context + SupervisorJob(context[Job])) - private var job: Job? = null - private var cancellationJob: Job? = null - - override fun onActive() { - super.onActive() - cancellationJob?.cancel() - cancellationJob = null - if (job?.isActive == true) { - return - } - job = scope.launch { - flow.collect(Collector()) - } - } - - override fun onInactive() { - super.onInactive() - cancellationJob?.cancel() - cancellationJob = scope.launch(Dispatchers.Main.immediate) { - delay(timeoutInMs) - if (!hasActiveObservers()) { - job?.cancel() - job = null - } - } - } - - private inner class Collector : FlowCollector { - - private var previousValue: Any? = value - private val dispatcher = Dispatchers.Main.immediate - - override suspend fun emit(value: T) { - if (previousValue != value) { - previousValue = value - if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { - withContext(dispatcher) { - setValue(value) - } - } else { - setValue(value) - } - } - } - } -} - -fun Flow.asFlowLiveData( - context: CoroutineContext = EmptyCoroutineContext, - defaultValue: T, - timeoutInMs: Long = DEFAULT_TIMEOUT, -): LiveData = FlowLiveData(this, defaultValue, context, timeoutInMs) - -fun StateFlow.asFlowLiveData( - context: CoroutineContext = EmptyCoroutineContext, - timeoutInMs: Long = DEFAULT_TIMEOUT, -): LiveData = FlowLiveData(this, value, context, timeoutInMs) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/SingleLiveEvent.kt b/app/src/main/java/org/koitharu/kotatsu/utils/SingleLiveEvent.kt deleted file mode 100644 index cbc89d96b..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/SingleLiveEvent.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.koitharu.kotatsu.utils - -import androidx.annotation.AnyThread -import androidx.annotation.MainThread -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.EmptyCoroutineContext - -class SingleLiveEvent : LiveData() { - - private val pending = AtomicBoolean(false) - - override fun observe(owner: LifecycleOwner, observer: Observer) { - super.observe(owner) { - if (pending.compareAndSet(true, false)) { - observer.onChanged(it) - } - } - } - - override fun setValue(value: T) { - pending.set(true) - super.setValue(value) - } - - @MainThread - fun call(newValue: T) { - setValue(newValue) - } - - @AnyThread - fun postCall(newValue: T) { - postValue(newValue) - } - - suspend fun emitCall(newValue: T) { - val dispatcher = Dispatchers.Main.immediate - if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { - withContext(dispatcher) { - setValue(newValue) - } - } else { - setValue(newValue) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt b/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt deleted file mode 100644 index ee84cffb2..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.koitharu.kotatsu.utils - -import android.app.Activity - -class TaggedActivityResult( - val tag: String, - val result: Int, -) - -val TaggedActivityResult.isSuccess: Boolean - get() = this.result == Activity.RESULT_OK \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LayoutManagerExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LayoutManagerExt.kt deleted file mode 100644 index 57e9c4a8f..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LayoutManagerExt.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.StaggeredGridLayoutManager - -internal val RecyclerView.LayoutManager?.firstVisibleItemPosition - get() = when (this) { - is LinearLayoutManager -> findFirstVisibleItemPosition() - is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0] - else -> 0 - } - -internal val RecyclerView.LayoutManager?.isLayoutReversed - get() = when (this) { - is LinearLayoutManager -> reverseLayout - is StaggeredGridLayoutManager -> reverseLayout - else -> false - } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt deleted file mode 100644 index 7f23b9487..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.utils.BufferedObserver -import kotlin.coroutines.EmptyCoroutineContext - -fun LiveData.requireValue(): T = checkNotNull(value) { - "LiveData value is null" -} - -fun LiveData.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver) { - var previous: T? = null - this.observe(owner) { - observer.onChanged(it, previous) - previous = it - } -} - -suspend fun MutableLiveData.emitValue(newValue: T) { - val dispatcher = Dispatchers.Main.immediate - if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { - withContext(dispatcher) { - value = newValue - } - } else { - value = newValue - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt deleted file mode 100644 index 61066cd5d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -@Suppress("UNCHECKED_CAST") -fun Class.castOrNull(obj: Any?): T? { - if (obj == null || !isInstance(obj)) { - return null - } - return obj as T -} diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 95d48afa2..164095e81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -20,12 +20,12 @@ import org.acra.ktx.initAcra import org.acra.sender.HttpSender import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.WorkServiceStopHelper +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.PagesCache -import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.reader.domain.PageLoader -import org.koitharu.kotatsu.utils.WorkServiceStopHelper -import org.koitharu.kotatsu.utils.ext.processLifecycleScope import javax.inject.Inject @HiltAndroidApp diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt index 076b19a3c..0f7533857 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -1,6 +1,10 @@ package org.koitharu.kotatsu.bookmarks.data -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaWithTags @@ -18,7 +22,7 @@ abstract class BookmarksDao { @Transaction @Query( - "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at" + "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at", ) abstract fun observe(): Flow>> @@ -29,5 +33,8 @@ abstract class BookmarksDao { abstract suspend fun delete(entity: BookmarkEntity) @Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId") - abstract suspend fun delete(mangaId: Long, pageId: Long) -} \ No newline at end of file + abstract suspend fun delete(mangaId: Long, pageId: Long): Int + + @Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page") + abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int +} diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt similarity index 73% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt index 5b6ff3bf0..98116f8a4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt @@ -1,7 +1,8 @@ package org.koitharu.kotatsu.bookmarks.domain import org.koitharu.kotatsu.parsers.model.Manga -import java.util.* +import org.koitharu.kotatsu.parsers.model.MangaPage +import java.util.Date class Bookmark( val manga: Manga, @@ -14,6 +15,20 @@ class Bookmark( val percent: Float, ) { + val directImageUrl: String? + get() = if (isImageUrlDirect()) imageUrl else null + + fun toMangaPage() = MangaPage( + id = pageId, + url = imageUrl, + preview = null, + source = manga.source, + ) + + private fun isImageUrlDirect(): Boolean { + return imageUrl.substringAfterLast('.').length in 2..4 + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -27,9 +42,7 @@ class Bookmark( if (scroll != other.scroll) return false if (imageUrl != other.imageUrl) return false if (createdAt != other.createdAt) return false - if (percent != other.percent) return false - - return true + return percent == other.percent } override fun hashCode(): Int { @@ -43,4 +56,4 @@ class Bookmark( result = 31 * result + percent.hashCode() return result } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt index feeac4519..fab2180f3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt @@ -5,7 +5,6 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity import org.koitharu.kotatsu.bookmarks.data.toBookmark import org.koitharu.kotatsu.bookmarks.data.toBookmarks @@ -14,9 +13,10 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.mapItems +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import javax.inject.Inject @Reusable @@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor( } } - suspend fun removeBookmark(mangaId: Long, pageId: Long) { - db.bookmarksDao.delete(mangaId, pageId) + suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) { + check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) { + "Bookmark not found" + } + } + + suspend fun removeBookmark(bookmark: Bookmark) { + removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page) } suspend fun removeBookmarks(ids: Map>): ReversibleHandle { diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt index 1bcc16d2e..5f2f952bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksActivity.kt @@ -3,16 +3,14 @@ package org.koitharu.kotatsu.bookmarks.ui import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.ViewGroup import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.graphics.Insets -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.commit import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner @@ -24,10 +22,10 @@ class BookmarksActivity : SnackbarOwner { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override val snackbarHost: CoordinatorLayout - get() = binding.root + get() = viewBinding.root override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -43,7 +41,7 @@ class BookmarksActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt index 06b3dbb36..cbc9656f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksFragment.kt @@ -16,19 +16,23 @@ import coil.ImageLoader import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.reverseAsync -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.bookmarks.data.ids import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.ui.util.reverseAsync +import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener @@ -37,8 +41,6 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.reader.ui.ReaderActivity -import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import javax.inject.Inject @AndroidEntryPoint @@ -56,12 +58,12 @@ class BookmarksFragment : private var adapter: BookmarksGroupAdapter? = null private var selectionController: SectionedSelectionController? = null - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding { return FragmentListSimpleBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) selectionController = SectionedSelectionController( activity = requireActivity(), owner = this, @@ -77,12 +79,12 @@ class BookmarksFragment : ) binding.recyclerView.adapter = adapter binding.recyclerView.setHasFixedSize(true) - val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) + val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)) binding.recyclerView.addItemDecoration(spacingDecoration) viewModel.content.observe(viewLifecycleOwner, ::onListChanged) - viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone) } override fun onDestroyView() { @@ -93,8 +95,11 @@ class BookmarksFragment : override fun onItemClick(item: Bookmark, view: View) { if (selectionController?.onItemClick(item.manga, item.pageId) != true) { - val intent = ReaderActivity.newIntent(view.context, item) - startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) + val intent = ReaderActivity.IntentBuilder(view.context) + .bookmark(item) + .incognito(true) + .build() + startActivity(intent, scaleUpActivityOptionsOf(view)) Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() } } @@ -114,7 +119,7 @@ class BookmarksFragment : override fun onFastScrollStop(fastScroller: FastScroller) = Unit override fun onSelectionChanged(controller: SectionedSelectionController, count: Int) { - binding.recyclerView.invalidateNestedItemDecorations() + requireViewBinding().recyclerView.invalidateNestedItemDecorations() } override fun onCreateActionMode( @@ -149,10 +154,10 @@ class BookmarksFragment : ): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext()) override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) - binding.recyclerView.fastScroller.updateLayoutParams { + requireViewBinding().recyclerView.fastScroller.updateLayoutParams { bottomMargin = insets.bottom } } diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt index 025acb882..85886eed8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksSelectionDecoration.kt @@ -4,8 +4,8 @@ import android.content.Context import android.view.View import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.util.ext.getItem import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration -import org.koitharu.kotatsu.utils.ext.getItem class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { @@ -14,5 +14,4 @@ class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration( val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID return item.pageId } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt index ced4e03cc..d08ce07b4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/BookmarksViewModel.kt @@ -1,23 +1,26 @@ package org.koitharu.kotatsu.bookmarks.ui -import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel @@ -25,9 +28,9 @@ class BookmarksViewModel @Inject constructor( private val repository: BookmarksRepository, ) : BaseViewModel() { - val onActionDone = SingleLiveEvent() + val onActionDone = MutableEventFlow() - val content: LiveData> = repository.observeBookmarks() + val content: StateFlow> = repository.observeBookmarks() .map { list -> if (list.isEmpty()) { listOf( @@ -43,12 +46,12 @@ class BookmarksViewModel @Inject constructor( } } .catch { e -> emit(listOf(e.toErrorState(canRetry = false))) } - .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) fun removeBookmarks(ids: Map>) { launchJob(Dispatchers.Default) { val handle = repository.removeBookmarks(ids) - onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle)) + onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle)) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt index 1ffa3bac3..5996df31d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt @@ -4,16 +4,16 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.decodeRegion +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemBookmarkBinding -import org.koitharu.kotatsu.utils.ext.decodeRegion -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun bookmarkListAD( coil: ImageLoader, @@ -28,7 +28,8 @@ fun bookmarkListAD( binding.root.setOnLongClickListener(listener) bind { - binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageUrl)?.run { + val data: Any = item.directImageUrl ?: item.toMangaPage() + binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run { size(CoverSizeResolver(binding.imageViewThumb)) placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt index 2f3022b8e..8d15f00ab 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksAdapter.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener class BookmarksAdapter( coil: ImageLoader, @@ -13,13 +13,15 @@ class BookmarksAdapter( clickListener: OnListItemClickListener, ) : AsyncListDifferDelegationAdapter( DiffCallback(), - bookmarkListAD(coil, lifecycleOwner, clickListener) + bookmarkListAD(coil, lifecycleOwner, clickListener), ) { private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { - return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId + return oldItem.manga.id == newItem.manga.id && + oldItem.chapterId == newItem.chapterId && + oldItem.page == newItem.page } override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { @@ -27,4 +29,4 @@ class BookmarksAdapter( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt index a4d33d0eb..df737ff54 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAD.kt @@ -6,20 +6,20 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.util.ext.clearItemDecorations +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.clearItemDecorations -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun bookmarksGroupAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt index a73d0a0c1..31ab12fd7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarksGroupAdapter.kt @@ -5,16 +5,17 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD 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.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.parsers.model.Manga import kotlin.jvm.internal.Intrinsics @@ -54,6 +55,10 @@ class BookmarksGroupAdapter( oldItem.manga.id == newItem.manga.id } + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + else -> oldItem.javaClass == newItem.javaClass } } diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/model/BookmarksGroup.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt index 55f4e193b..7d9217ea5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -12,10 +12,10 @@ import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.core.view.updatePadding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability import org.koitharu.kotatsu.databinding.ActivityBrowserBinding -import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability import com.google.android.material.R as materialR @SuppressLint("SetJavaScriptEnabled") @@ -32,13 +32,13 @@ class BrowserActivity : BaseActivity(), BrowserCallback setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } - with(binding.webView.settings) { + with(viewBinding.webView.settings) { javaScriptEnabled = true userAgentString = CommonHeadersInterceptor.userAgentChrome } - binding.webView.webViewClient = BrowserClient(this) - binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(binding.webView) + viewBinding.webView.webViewClient = BrowserClient(this) + viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) onBackPressedDispatcher.addCallback(onBackPressedCallback) if (savedInstanceState != null) { return @@ -51,18 +51,18 @@ class BrowserActivity : BaseActivity(), BrowserCallback intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), url, ) - binding.webView.loadUrl(url) + viewBinding.webView.loadUrl(url) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - binding.webView.saveState(outState) + viewBinding.webView.saveState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - binding.webView.restoreState(savedInstanceState) + viewBinding.webView.restoreState(savedInstanceState) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -73,14 +73,14 @@ class BrowserActivity : BaseActivity(), BrowserCallback override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { - binding.webView.stopLoading() + viewBinding.webView.stopLoading() finishAfterTransition() true } R.id.action_browser -> { val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(binding.webView.url) + intent.data = Uri.parse(viewBinding.webView.url) try { startActivity(Intent.createChooser(intent, item.title)) } catch (_: ActivityNotFoundException) { @@ -92,22 +92,23 @@ class BrowserActivity : BaseActivity(), BrowserCallback } override fun onPause() { - binding.webView.onPause() + viewBinding.webView.onPause() super.onPause() } override fun onResume() { super.onResume() - binding.webView.onResume() + viewBinding.webView.onResume() } override fun onDestroy() { super.onDestroy() - binding.webView.destroy() + viewBinding.webView.stopLoading() + viewBinding.webView.destroy() } override fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading + viewBinding.progressBar.isVisible = isLoading } override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { @@ -120,10 +121,10 @@ class BrowserActivity : BaseActivity(), BrowserCallback } override fun onWindowInsetsChanged(insets: Insets) { - binding.appbar.updatePadding( + viewBinding.appbar.updatePadding( top = insets.top, ) - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, bottom = insets.bottom, diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/BrowserCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/OnHistoryChangedListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/OnHistoryChangedListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/OnHistoryChangedListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/OnHistoryChangedListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/ProgressChromeClient.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/ProgressChromeClient.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/ProgressChromeClient.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/WebViewBackPressedCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/WebViewBackPressedCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/WebViewBackPressedCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/WebViewBackPressedCallback.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt new file mode 100644 index 000000000..51d29fc9f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt @@ -0,0 +1,175 @@ +package org.koitharu.kotatsu.browser.cloudflare + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.webkit.CookieManager +import android.webkit.WebSettings +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.graphics.Insets +import androidx.core.net.toUri +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import dagger.hilt.android.AndroidEntryPoint +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.browser.WebViewBackPressedCallback +import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor +import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.TaggedActivityResult +import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability +import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import javax.inject.Inject +import com.google.android.material.R as materialR + +@AndroidEntryPoint +class CloudFlareActivity : BaseActivity(), CloudFlareCallback { + + private var pendingResult = RESULT_CANCELED + + @Inject + lateinit var cookieJar: MutableCookieJar + + private var onBackPressedCallback: WebViewBackPressedCallback? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) { + return + } + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) + } + val url = intent?.dataString.orEmpty() + with(viewBinding.webView.settings) { + javaScriptEnabled = true + cacheMode = WebSettings.LOAD_DEFAULT + domStorageEnabled = true + databaseEnabled = true + userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback + } + viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also { + onBackPressedDispatcher.addCallback(it) + } + CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) + if (savedInstanceState != null) { + return + } + if (url.isEmpty()) { + finishAfterTransition() + } else { + onTitleChanged(getString(R.string.loading_), url) + viewBinding.webView.loadUrl(url) + } + } + + override fun onDestroy() { + viewBinding.webView.run { + stopLoading() + destroy() + } + super.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + viewBinding.webView.saveState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + viewBinding.webView.restoreState(savedInstanceState) + } + + override fun onWindowInsetsChanged(insets: Insets) { + viewBinding.appbar.updatePadding( + top = insets.top, + ) + viewBinding.root.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom, + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + viewBinding.webView.stopLoading() + finishAfterTransition() + true + } + + else -> super.onOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + viewBinding.webView.onResume() + } + + override fun onPause() { + viewBinding.webView.onPause() + super.onPause() + } + + override fun finish() { + setResult(pendingResult) + super.finish() + } + + override fun onPageLoaded() { + viewBinding.progressBar.isInvisible = true + } + + override fun onCheckPassed() { + pendingResult = RESULT_OK + finishAfterTransition() + } + + override fun onLoadingStateChanged(isLoading: Boolean) { + viewBinding.progressBar.isVisible = isLoading + } + + override fun onHistoryChanged() { + onBackPressedCallback?.onHistoryChanged() + } + + override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { + setTitle(title) + supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle + } + + class Contract : ActivityResultContract, TaggedActivityResult>() { + override fun createIntent(context: Context, input: Pair): Intent { + return newIntent(context, input.first, input.second) + } + + override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { + return TaggedActivityResult(TAG, resultCode) + } + } + + companion object { + + const val TAG = "CloudFlareActivity" + private const val ARG_UA = "ua" + + fun newIntent( + context: Context, + url: String, + headers: Headers?, + ) = Intent(context, CloudFlareActivity::class.java).apply { + data = url.toUri() + headers?.get(CommonHeaders.USER_AGENT)?.let { + putExtra(ARG_UA, it) + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index 97542c58d..25332e92e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -4,7 +4,6 @@ import android.app.Application import android.content.Context import android.provider.SearchRecentSuggestions import android.text.Html -import android.util.AndroidRuntimeException import androidx.collection.arraySetOf import androidx.room.InvalidationTracker import coil.ComponentRegistry @@ -23,51 +22,41 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import okhttp3.Cache -import okhttp3.CookieJar import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.cache.StubContentCache import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.network.* -import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar -import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar -import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher -import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.image.CoilImageGetter +import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle +import org.koitharu.kotatsu.core.util.IncognitoModeIndicator +import org.koitharu.kotatsu.core.util.ext.activityManager +import org.koitharu.kotatsu.core.util.ext.connectivityManager +import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CbzFetcher -import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.sync.domain.SyncController -import org.koitharu.kotatsu.utils.IncognitoModeIndicator -import org.koitharu.kotatsu.utils.ext.activityManager -import org.koitharu.kotatsu.utils.ext.connectivityManager -import org.koitharu.kotatsu.utils.ext.isLowRamDevice -import org.koitharu.kotatsu.utils.image.CoilImageGetter import org.koitharu.kotatsu.widget.WidgetUpdater -import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) interface AppModule { - @Binds - fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar - @Binds fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext @@ -76,53 +65,6 @@ interface AppModule { companion object { - @Provides - @Singleton - fun provideCookieJar( - @ApplicationContext context: Context - ): MutableCookieJar = try { - AndroidCookieJar() - } catch (e: AndroidRuntimeException) { - // WebView is not available - PreferencesCookieJar(context) - } - - @Provides - @Singleton - fun provideHttpCache( - localStorageManager: LocalStorageManager, - ): Cache = localStorageManager.createHttpCache() - - @Provides - @Singleton - fun provideOkHttpClient( - cache: Cache, - commonHeadersInterceptor: CommonHeadersInterceptor, - mirrorSwitchInterceptor: MirrorSwitchInterceptor, - cookieJar: CookieJar, - settings: AppSettings, - ): OkHttpClient { - return OkHttpClient.Builder().apply { - connectTimeout(20, TimeUnit.SECONDS) - readTimeout(60, TimeUnit.SECONDS) - writeTimeout(20, TimeUnit.SECONDS) - cookieJar(cookieJar) - dns(DoHManager(cache, settings)) - if (settings.isSSLBypassEnabled) { - bypassSSLErrors() - } - cache(cache) - addNetworkInterceptor(CacheLimitInterceptor()) - addInterceptor(GZipInterceptor()) - addInterceptor(commonHeadersInterceptor) - addInterceptor(CloudFlareInterceptor()) - addInterceptor(mirrorSwitchInterceptor) - if (BuildConfig.DEBUG) { - addInterceptor(CurlLoggingInterceptor()) - } - }.build() - } - @Provides @Singleton fun provideNetworkState( @@ -141,14 +83,11 @@ interface AppModule { @Singleton fun provideCoil( @ApplicationContext context: Context, - okHttpClient: OkHttpClient, + @MangaHttpClient okHttpClient: OkHttpClient, mangaRepositoryFactory: MangaRepository.Factory, + imageProxyInterceptor: ImageProxyInterceptor, + pageFetcherFactory: MangaPageFetcher.Factory, ): ImageLoader { - val httpClientFactory = { - okHttpClient.newBuilder() - .cache(null) - .build() - } val diskCacheFactory = { val rootDir = context.externalCacheDir ?: context.cacheDir DiskCache.Builder() @@ -156,19 +95,21 @@ interface AppModule { .build() } return ImageLoader.Builder(context) - .okHttpClient(httpClientFactory) + .okHttpClient(okHttpClient.newBuilder().cache(null).build()) .interceptorDispatcher(Dispatchers.Default) .fetcherDispatcher(Dispatchers.IO) .decoderDispatcher(Dispatchers.Default) .transformationDispatcher(Dispatchers.Default) .diskCache(diskCacheFactory) .logger(if (BuildConfig.DEBUG) DebugLogger() else null) - .allowRgb565(isLowRamDevice(context)) + .allowRgb565(context.isLowRamDevice()) .components( ComponentRegistry.Builder() .add(SvgDecoder.Factory()) .add(CbzFetcher.Factory()) .add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory)) + .add(pageFetcherFactory) + .add(imageProxyInterceptor) .build(), ).build() } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupEntry.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/BackupEntry.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 9d80b7af4..f02602cef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -7,7 +7,7 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.mapJSON -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject private const val PAGE_SIZE = 10 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipInput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipInput.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt index 8a6217d04..c06f45a76 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt @@ -5,10 +5,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okio.Closeable import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.format import org.koitharu.kotatsu.core.zip.ZipOutput -import org.koitharu.kotatsu.utils.ext.format import java.io.File -import java.util.* +import java.util.Date +import java.util.Locale import java.util.zip.Deflater class BackupZipOutput(val file: File) : Closeable { @@ -42,4 +43,4 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl append(".bk.zip") } BackupZipOutput(File(dir, filename)) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/CompositeResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/CompositeResult.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ContentCache.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ContentCache.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringValue.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringValue.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringValue.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringValue.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/SafeDeferred.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/SafeDeferred.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/StubContentCache.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/cache/StubContentCache.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt index d390e80dd..d70dcb983 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration11To12 import org.koitharu.kotatsu.core.db.migrations.Migration12To13 import org.koitharu.kotatsu.core.db.migrations.Migration13To14 import org.koitharu.kotatsu.core.db.migrations.Migration14To15 +import org.koitharu.kotatsu.core.db.migrations.Migration15To16 import org.koitharu.kotatsu.core.db.migrations.Migration1To2 import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration3To4 @@ -33,6 +34,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration6To7 import org.koitharu.kotatsu.core.db.migrations.Migration7To8 import org.koitharu.kotatsu.core.db.migrations.Migration8To9 import org.koitharu.kotatsu.core.db.migrations.Migration9To10 +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity @@ -46,9 +48,8 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TracksDao -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -const val DATABASE_VERSION = 15 +const val DATABASE_VERSION = 16 @Database( entities = [ @@ -100,6 +101,7 @@ val databaseMigrations: Array Migration12To13(), Migration13To14(), Migration14To15(), + Migration15To16(), ) fun MangaDatabase(context: Context): MangaDatabase = Room diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/Tables.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/Tables.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/PreferencesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/PreferencesDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/dao/PreferencesDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/PreferencesDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TagsDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TagsDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt index 7bba6f4df..80bcb6045 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt @@ -1,13 +1,13 @@ package org.koitharu.kotatsu.core.db.entity import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase -import org.koitharu.kotatsu.utils.ext.longHashCode // Entity to model diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt index a5ccdb765..98512e8b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaPrefsEntity.kt @@ -23,4 +23,5 @@ data class MangaPrefsEntity( @ColumnInfo(name = "mode") val mode: Int, @ColumnInfo(name = "cf_brightness") val cfBrightness: Float, @ColumnInfo(name = "cf_contrast") val cfContrast: Float, + @ColumnInfo(name = "cf_invert") val cfInvert: Boolean, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/TagEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/TagEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration12To13.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration12To13.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration12To13.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration12To13.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration13To14.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration13To14.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration13To14.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration13To14.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration14To15.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration14To15.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration14To15.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration14To15.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration15To16.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration15To16.kt new file mode 100644 index 000000000..aba52e885 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration15To16.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration15To16 : Migration(15, 16) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0") + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration1To2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration1To2.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration1To2.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration1To2.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration2To3.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration2To3.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration2To3.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration2To3.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration3To4.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration3To4.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration3To4.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration3To4.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration4To5.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration4To5.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration4To5.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration4To5.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration5To6.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration5To6.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration5To6.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration5To6.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration6To7.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration6To7.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration6To7.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration6To7.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration7To8.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration7To8.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration7To8.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration7To8.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CaughtException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CaughtException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CompositeException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CompositeException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/EmptyHistoryException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/EmptyHistoryException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/EmptyHistoryException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/EmptyHistoryException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/SyncApiException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/SyncApiException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/SyncApiException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/SyncApiException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/UnsupportedFileException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/WrongPasswordException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/WrongPasswordException.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/WrongPasswordException.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/WrongPasswordException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt index c3bc2f893..1edf3b662 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/DialogErrorObserver.kt @@ -6,9 +6,9 @@ import androidx.core.util.Consumer import androidx.fragment.app.Fragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.ErrorDetailsDialog +import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.parsers.exception.ParseException -import org.koitharu.kotatsu.utils.ext.getDisplayMessage class DialogErrorObserver( host: View, @@ -22,10 +22,7 @@ class DialogErrorObserver( fragment: Fragment?, ) : this(host, fragment, null, null) - override fun onChanged(value: Throwable?) { - if (value == null) { - return - } + override suspend fun emit(value: Throwable) { val listener = DialogListener(value) val dialogBuilder = MaterialAlertDialogBuilder(activity ?: host.context) .setMessage(value.getDisplayMessage(host.context.resources)) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt index e41b65955..a64fb9edf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ErrorObserver.kt @@ -7,19 +7,19 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer import androidx.lifecycle.coroutineScope +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import org.koitharu.kotatsu.utils.ext.findActivity -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.findActivity +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope abstract class ErrorObserver( protected val host: View, protected val fragment: Fragment?, private val resolver: ExceptionResolver?, private val onResolved: Consumer?, -) : Observer { +) : FlowCollector { protected val activity = host.context.findActivity() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt index 75d466bbb..9407ab6e9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt @@ -6,37 +6,41 @@ import androidx.annotation.StringRes import androidx.collection.ArrayMap import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Headers import org.koitharu.kotatsu.R import org.koitharu.kotatsu.browser.BrowserActivity -import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog +import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException -import org.koitharu.kotatsu.core.ui.ErrorDetailsDialog +import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog +import org.koitharu.kotatsu.core.util.TaggedActivityResult import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity -import org.koitharu.kotatsu.utils.TaggedActivityResult -import org.koitharu.kotatsu.utils.isSuccess import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -class ExceptionResolver private constructor( - private val activity: FragmentActivity?, - private val fragment: Fragment?, -) : ActivityResultCallback { +class ExceptionResolver : ActivityResultCallback { private val continuations = ArrayMap>(1) - private lateinit var sourceAuthContract: ActivityResultLauncher - - constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) { + private val activity: FragmentActivity? + private val fragment: Fragment? + private val sourceAuthContract: ActivityResultLauncher + private val cloudflareContract: ActivityResultLauncher> + + constructor(activity: FragmentActivity) { + this.activity = activity + fragment = null sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this) + cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this) } - constructor(fragment: Fragment) : this(activity = null, fragment = fragment) { + constructor(fragment: Fragment) { + this.fragment = fragment + activity = null sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this) + cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this) } override fun onActivityResult(result: TaggedActivityResult) { @@ -58,22 +62,9 @@ class ExceptionResolver private constructor( else -> false } - private suspend fun resolveCF(url: String, headers: Headers): Boolean { - val dialog = CloudFlareDialog.newInstance(url, headers) - val fm = getFragmentManager() - return suspendCancellableCoroutine { cont -> - fm.clearFragmentResult(CloudFlareDialog.TAG) - continuations[CloudFlareDialog.TAG] = cont - fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result -> - continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT)) - } - dialog.show(fm, CloudFlareDialog.TAG) - cont.invokeOnCancellation { - continuations.remove(CloudFlareDialog.TAG, cont) - fm.clearFragmentResultListener(CloudFlareDialog.TAG) - dialog.dismissAllowingStateLoss() - } - } + private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont -> + continuations[CloudFlareActivity.TAG] = cont + cloudflareContract.launch(url to headers) } private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont -> diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt index fb3cea7d9..e39897cfc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/SnackbarErrorObserver.kt @@ -5,10 +5,10 @@ import androidx.core.util.Consumer import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.ErrorDetailsDialog +import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.exception.ParseException -import org.koitharu.kotatsu.utils.ext.getDisplayMessage class SnackbarErrorObserver( host: View, @@ -22,10 +22,7 @@ class SnackbarErrorObserver( fragment: Fragment?, ) : this(host, fragment, null, null) - override fun onChanged(value: Throwable?) { - if (value == null) { - return - } + override suspend fun emit(value: Throwable) { val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT) if (activity is BottomNavOwner) { snackbar.anchorView = activity.bottomNav diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt index 70598cb0a..4d26db92a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt @@ -13,14 +13,15 @@ import okhttp3.Request import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.network.BaseHttpClient import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.asArrayList +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull import org.koitharu.kotatsu.parsers.util.parseJsonArray -import org.koitharu.kotatsu.utils.ext.asArrayList -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import java.io.ByteArrayInputStream import java.io.InputStream import java.security.MessageDigest @@ -36,7 +37,7 @@ private const val CONTENT_TYPE_APK = "application/vnd.android.package-archive" class AppUpdateRepository @Inject constructor( @ApplicationContext private val context: Context, private val settings: AppSettings, - private val okHttp: OkHttpClient, + @BaseHttpClient private val okHttp: OkHttpClient, ) { private val availableUpdate = MutableStateFlow(null) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppVersion.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppVersion.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/VersionId.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/github/VersionId.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/logs/FileLogger.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/logs/FileLogger.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/core/logs/FileLogger.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/logs/FileLogger.kt index 8c46d4c01..ee75e9e21 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/logs/FileLogger.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/logs/FileLogger.kt @@ -14,10 +14,10 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.subdir +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.core.util.ext.subdir +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import java.io.FileOutputStream import java.text.SimpleDateFormat diff --git a/app/src/main/java/org/koitharu/kotatsu/core/logs/Loggers.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/logs/Loggers.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/logs/Loggers.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/logs/Loggers.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/logs/LoggersModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/logs/LoggersModule.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/logs/LoggersModule.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/logs/LoggersModule.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/FavouriteCategory.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/FavouriteCategory.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 47ac983f3..ce0b3e7f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -1,11 +1,12 @@ package org.koitharu.kotatsu.core.model import androidx.core.os.LocaleListCompat +import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.utils.ext.iterator fun Collection.ids() = mapToSet { it.id } @@ -23,6 +24,10 @@ fun Collection.countChaptersByBranch(): Int { return acc.values.max() } +fun Manga.findChapter(id: Long): MangaChapter? { + return chapters?.find { it.id == id } +} + fun Manga.getPreferredBranch(history: MangaHistory?): String? { val ch = chapters if (ch.isNullOrEmpty()) { @@ -54,3 +59,6 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? { } return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key } + +val Manga.isLocal: Boolean + get() = source == MangaSource.LOCAL diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaHistory.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaHistory.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/ZoomMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/ZoomMode.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/ZoomMode.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/ZoomMode.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt index 7ed9f638f..e774ce82e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt @@ -2,12 +2,12 @@ package org.koitharu.kotatsu.core.model.parcelable import android.os.Parcel import androidx.core.os.ParcelCompat +import org.koitharu.kotatsu.core.util.ext.readParcelableCompat +import org.koitharu.kotatsu.core.util.ext.readSerializableCompat import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.readParcelableCompat -import org.koitharu.kotatsu.utils.ext.readSerializableCompat fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) { out.writeLong(id) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt index bd5490e0a..7f6cf2f42 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt @@ -2,15 +2,15 @@ package org.koitharu.kotatsu.core.model.parcelable import android.os.Parcel import android.os.Parcelable +import org.koitharu.kotatsu.core.util.ext.Set import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.Set class ParcelableMangaTags( val tags: Set, ) : Parcelable { constructor(parcel: Parcel) : this( - Set(parcel.readInt()) { parcel.readMangaTag() } + Set(parcel.readInt()) { parcel.readMangaTag() }, ) override fun writeToParcel(parcel: Parcel, flags: Int) { @@ -33,4 +33,4 @@ class ParcelableMangaTags( return arrayOfNulls(size) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt new file mode 100644 index 000000000..87a410ba6 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.core.network + +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI + +class AppProxySelector( + private val settings: AppSettings, +) : ProxySelector() { + + init { + setDefault(this) + } + + private var cachedProxy: Proxy? = null + + override fun select(uri: URI?): List { + return listOf(getProxy()) + } + + override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) { + ioe?.printStackTraceDebug() + } + + private fun getProxy(): Proxy { + val type = settings.proxyType + val address = settings.proxyAddress + val port = settings.proxyPort + if (type == Proxy.Type.DIRECT || address.isNullOrEmpty() || port == 0) { + return Proxy.NO_PROXY + } + cachedProxy?.let { + val addr = it.address() as? InetSocketAddress + if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) { + return it + } + } + val proxy = Proxy(type, InetSocketAddress(address, port)) + cachedProxy = proxy + return proxy + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeaders.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeaders.kt index f8976acd6..1454542ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeaders.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeaders.kt @@ -14,6 +14,7 @@ object CommonHeaders { const val ACCEPT_ENCODING = "Accept-Encoding" const val AUTHORIZATION = "Authorization" const val CACHE_CONTROL = "Cache-Control" + const val PROXY_AUTHORIZATION = "Proxy-Authorization" val CACHE_CONTROL_NO_STORE: CacheControl get() = CacheControl.Builder().noStore().build() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt index 8a19866cd..979dcd7f2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CommonHeadersInterceptor.kt @@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mergeWith -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.net.IDN import java.util.Locale import javax.inject.Inject diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt index f32717aad..0cc4b6db0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/DoHManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHManager.kt @@ -6,7 +6,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.net.InetAddress import java.net.UnknownHostException @@ -52,8 +52,9 @@ class DoHManager( tryGetByIp("8.8.8.8"), tryGetByIp("2001:4860:4860::8888"), tryGetByIp("2001:4860:4860::8844"), - ) + ), ).build() + DoHProvider.CLOUDFLARE -> DnsOverHttps.Builder().client(bootstrapClient) .url("https://cloudflare-dns.com/dns-query".toHttpUrl()) .resolvePrivateAddresses(true) @@ -68,8 +69,9 @@ class DoHManager( tryGetByIp("2606:4700:4700::1001"), tryGetByIp("2606:4700:4700::0064"), tryGetByIp("2606:4700:4700::6400"), - ) + ), ).build() + DoHProvider.ADGUARD -> DnsOverHttps.Builder().client(bootstrapClient) .url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl()) .resolvePrivateAddresses(true) @@ -79,7 +81,7 @@ class DoHManager( tryGetByIp("94.140.14.141"), tryGetByIp("2a10:50c0::1:ff"), tryGetByIp("2a10:50c0::2:ff"), - ) + ), ).build() } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/DoHProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/DoHProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/DoHProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/GZipInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/GZipInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/GZipInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/GZipInterceptor.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/HttpClients.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/HttpClients.kt new file mode 100644 index 000000000..fb69fe291 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/HttpClients.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.core.network + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class BaseHttpClient + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class MangaHttpClient diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt new file mode 100644 index 000000000..54423ba67 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ImageProxyInterceptor.kt @@ -0,0 +1,103 @@ +package org.koitharu.kotatsu.core.network + +import android.util.Log +import androidx.collection.ArraySet +import coil.intercept.Interceptor +import coil.request.ErrorResult +import coil.request.ImageResult +import coil.request.SuccessResult +import coil.size.Dimension +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.ensureSuccess +import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps +import org.koitharu.kotatsu.parsers.util.await +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import java.util.Collections +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ImageProxyInterceptor @Inject constructor( + private val settings: AppSettings, +) : Interceptor { + + private val blacklist = Collections.synchronizedSet(ArraySet()) + + override suspend fun intercept(chain: Interceptor.Chain): ImageResult { + val request = chain.request + if (!settings.isImagesProxyEnabled) { + return chain.proceed(request) + } + val url: HttpUrl? = when (val data = request.data) { + is HttpUrl -> data + is String -> data.toHttpUrlOrNull() + else -> null + } + if (url == null || !url.isHttpOrHttps || url.host in blacklist) { + return chain.proceed(request) + } + val newUrl = HttpUrl.Builder() + .scheme("https") + .host("wsrv.nl") + .addQueryParameter("url", url.toString()) + .addQueryParameter("fit", "outside") + .addQueryParameter("we", null) + val size = request.sizeResolver.size() + (size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) } + (size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) } + + val newRequest = request.newBuilder() + .data(newUrl.build()) + .build() + val result = chain.proceed(newRequest) + return if (result is SuccessResult) { + result + } else { + logDebug((result as? ErrorResult)?.throwable) + chain.proceed(request).also { + if (it is SuccessResult) { + blacklist.add(url.host) + } + } + } + } + + suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response { + if (!settings.isImagesProxyEnabled) { + return okHttp.newCall(request).await() + } + val sourceUrl = request.url + val targetUrl = HttpUrl.Builder() + .scheme("https") + .host("wsrv.nl") + .addQueryParameter("url", sourceUrl.toString()) + .addQueryParameter("we", null) + val newRequest = request.newBuilder() + .url(targetUrl.build()) + .build() + return runCatchingCancellable { + okHttp.doCall(newRequest) + }.recover { + logDebug(it) + okHttp.doCall(request).also { + blacklist.add(sourceUrl.host) + } + }.getOrThrow() + } + + private suspend fun OkHttpClient.doCall(request: Request): Response { + return newCall(request).await().ensureSuccess() + } + + private fun logDebug(e: Throwable?) { + if (BuildConfig.DEBUG) { + Log.w("ImageProxy", e.toString()) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt new file mode 100644 index 000000000..1c91966a0 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -0,0 +1,89 @@ +package org.koitharu.kotatsu.core.network + +import android.content.Context +import android.util.AndroidRuntimeException +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.Cache +import okhttp3.CookieJar +import okhttp3.OkHttpClient +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar +import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar +import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.local.data.LocalStorageManager +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface NetworkModule { + + @Binds + fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar + + companion object { + + @Provides + @Singleton + fun provideCookieJar( + @ApplicationContext context: Context + ): MutableCookieJar = try { + AndroidCookieJar() + } catch (e: AndroidRuntimeException) { + // WebView is not available + PreferencesCookieJar(context) + } + + @Provides + @Singleton + fun provideHttpCache( + localStorageManager: LocalStorageManager, + ): Cache = localStorageManager.createHttpCache() + + @Provides + @Singleton + @BaseHttpClient + fun provideBaseHttpClient( + cache: Cache, + cookieJar: CookieJar, + settings: AppSettings, + ): OkHttpClient = OkHttpClient.Builder().apply { + connectTimeout(20, TimeUnit.SECONDS) + readTimeout(60, TimeUnit.SECONDS) + writeTimeout(20, TimeUnit.SECONDS) + cookieJar(cookieJar) + proxySelector(AppProxySelector(settings)) + proxyAuthenticator(ProxyAuthenticator(settings)) + dns(DoHManager(cache, settings)) + if (settings.isSSLBypassEnabled) { + bypassSSLErrors() + } + cache(cache) + addInterceptor(GZipInterceptor()) + addInterceptor(CloudFlareInterceptor()) + if (BuildConfig.DEBUG) { + addInterceptor(CurlLoggingInterceptor()) + } + }.build() + + @Provides + @Singleton + @MangaHttpClient + fun provideMangaHttpClient( + @BaseHttpClient baseClient: OkHttpClient, + commonHeadersInterceptor: CommonHeadersInterceptor, + mirrorSwitchInterceptor: MirrorSwitchInterceptor, + ): OkHttpClient = baseClient.newBuilder().apply { + addNetworkInterceptor(CacheLimitInterceptor()) + addInterceptor(commonHeadersInterceptor) + addInterceptor(mirrorSwitchInterceptor) + }.build() + + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt new file mode 100644 index 000000000..fb4ffad7e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.core.network + +import okhttp3.Authenticator +import okhttp3.Credentials +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import org.koitharu.kotatsu.core.prefs.AppSettings +import java.net.PasswordAuthentication +import java.net.Proxy + +class ProxyAuthenticator( + private val settings: AppSettings, +) : Authenticator, java.net.Authenticator() { + + init { + setDefault(this) + } + + override fun authenticate(route: Route?, response: Response): Request? { + if (!isProxyEnabled()) { + return null + } + if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) { + return null + } + val login = settings.proxyLogin ?: return null + val password = settings.proxyPassword ?: return null + val credential = Credentials.basic(login, password) + return response.request.newBuilder() + .header(CommonHeaders.PROXY_AUTHORIZATION, credential) + .build() + } + + override fun getPasswordAuthentication(): PasswordAuthentication? { + if (!isProxyEnabled()) { + return null + } + val login = settings.proxyLogin ?: return null + val password = settings.proxyPassword ?: return null + return PasswordAuthentication(login, password.toCharArray()) + } + + private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/SSLBypass.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/SSLBypass.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/core/network/SSLBypass.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/SSLBypass.kt index ed1221613..d5ef6fd59 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/SSLBypass.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/SSLBypass.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.network import android.annotation.SuppressLint import okhttp3.OkHttpClient -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.security.SecureRandom import java.security.cert.X509Certificate import javax.net.ssl.SSLContext diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt index cce51f827..623824e57 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Cookie import okhttp3.HttpUrl -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug private const val PREFS_NAME = "cookies" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt index 0c3899bf6..63907bdde 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/NetworkState.kt @@ -6,8 +6,8 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import kotlinx.coroutines.flow.first -import org.koitharu.kotatsu.utils.MediatorStateFlow -import org.koitharu.kotatsu.utils.ext.isOnline +import org.koitharu.kotatsu.core.util.MediatorStateFlow +import org.koitharu.kotatsu.core.util.ext.isOnline class NetworkState( private val connectivityManager: ConnectivityManager, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index 1de7b7c35..62d431639 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -22,16 +22,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.db.TABLE_HISTORY +import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.ReaderActivity -import org.koitharu.kotatsu.utils.ext.getDrawableOrThrow -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import javax.inject.Inject import javax.inject.Singleton @@ -142,8 +142,9 @@ class ShortcutsUpdater @Inject constructor( .setLongLabel(manga.title) .setIcon(icon) .setIntent( - ReaderActivity.newIntent(context, manga.id) - .setAction(ReaderActivity.ACTION_MANGA_READ), + ReaderActivity.IntentBuilder(context) + .mangaId(manga.id) + .build(), ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/VoiceInputContract.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/os/VoiceInputContract.kt index ddb42ab45..15a6de48d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/VoiceInputContract.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.os import android.app.Activity import android.content.Context diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt similarity index 53% rename from app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt index 263253786..81b13ada2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaDataRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaDataRepository.kt @@ -1,43 +1,25 @@ -package org.koitharu.kotatsu.base.domain +package org.koitharu.kotatsu.core.parser -import android.graphics.BitmapFactory -import android.net.Uri -import android.util.Size import androidx.room.withTransaction import dagger.Reusable -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runInterruptible -import okhttp3.OkHttpClient -import okhttp3.Request import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTags -import org.koitharu.kotatsu.core.network.CommonHeaders -import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.reader.domain.ReaderColorFilter -import java.io.File -import java.io.InputStream -import java.util.zip.ZipFile import javax.inject.Inject -import kotlin.math.roundToInt - -private const val MIN_WEBTOON_RATIO = 2 @Reusable class MangaDataRepository @Inject constructor( - private val okHttpClient: OkHttpClient, private val db: MangaDatabase, ) { @@ -57,6 +39,7 @@ class MangaDataRepository @Inject constructor( entity.copy( cfBrightness = colorFilter?.brightness ?: 0f, cfContrast = colorFilter?.contrast ?: 0f, + cfInvert = colorFilter?.isInverted ?: false, ), ) } @@ -99,72 +82,18 @@ class MangaDataRepository @Inject constructor( } private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? { - return if (cfBrightness != 0f || cfContrast != 0f) { - ReaderColorFilter(cfBrightness, cfContrast) + return if (cfBrightness != 0f || cfContrast != 0f || cfInvert) { + ReaderColorFilter(cfBrightness, cfContrast, cfInvert) } else { null } } - /** - * Automatic determine type of manga by page size - * @return ReaderMode.WEBTOON if page is wide - */ - suspend fun determineMangaIsWebtoon(repository: MangaRepository, pages: List): Boolean { - val pageIndex = (pages.size * 0.3).roundToInt() - val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" } - val url = repository.getPageUrl(page) - val uri = Uri.parse(url) - val size = if (uri.scheme == "cbz") { - runInterruptible(Dispatchers.IO) { - val zip = ZipFile(uri.schemeSpecificPart) - val entry = zip.getEntry(uri.fragment) - zip.getInputStream(entry).use { - getBitmapSize(it) - } - } - } else { - val request = Request.Builder() - .url(url) - .get() - .tag(MangaSource::class.java, page.source) - .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) - .build() - okHttpClient.newCall(request).await().use { - runInterruptible(Dispatchers.IO) { - getBitmapSize(it.body?.byteStream()) - } - } - } - return size.width * MIN_WEBTOON_RATIO < size.height - } - private fun newEntity(mangaId: Long) = MangaPrefsEntity( mangaId = mangaId, mode = -1, cfBrightness = 0f, cfContrast = 0f, + cfInvert = false, ) - - companion object { - - suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { - val options = BitmapFactory.Options().apply { - inJustDecodeBounds = true - } - BitmapFactory.decodeFile(file.path, options)?.recycle() - options.outMimeType - } - - private fun getBitmapSize(input: InputStream?): Size { - val options = BitmapFactory.Options().apply { - inJustDecodeBounds = true - } - BitmapFactory.decodeStream(input, null, options)?.recycle() - val imageHeight: Int = options.outHeight - val imageWidth: Int = options.outWidth - check(imageHeight > 0 && imageWidth > 0) - return Size(imageWidth, imageHeight) - } - } } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaIntent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/base/domain/MangaIntent.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt index 55c34cb90..8ab582147 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaIntent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaIntent.kt @@ -1,39 +1,42 @@ -package org.koitharu.kotatsu.base.domain +package org.koitharu.kotatsu.core.parser import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.lifecycle.SavedStateHandle -import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.getParcelableCompat +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.getParcelableCompat -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat class MangaIntent private constructor( @JvmField val manga: Manga?, - @JvmField val mangaId: Long, + @JvmField val id: Long, @JvmField val uri: Uri?, ) { constructor(intent: Intent?) : this( manga = intent?.getParcelableExtraCompat(KEY_MANGA)?.manga, - mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE, + id = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE, uri = intent?.data, ) constructor(savedStateHandle: SavedStateHandle) : this( manga = savedStateHandle.get(KEY_MANGA)?.manga, - mangaId = savedStateHandle[KEY_ID] ?: ID_NONE, + id = savedStateHandle[KEY_ID] ?: ID_NONE, uri = savedStateHandle[BaseActivity.EXTRA_DATA], ) constructor(args: Bundle?) : this( manga = args?.getParcelableCompat(KEY_MANGA)?.manga, - mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE, + id = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE, uri = null, ) + val mangaId: Long + get() = if (id != ID_NONE) id else manga?.id ?: uri?.lastPathSegment?.toLongOrNull() ?: ID_NONE + companion object { const val ID_NONE = 0L diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt index 17f97c398..fe140190d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt @@ -9,12 +9,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.prefs.SourceSettings +import org.koitharu.kotatsu.core.util.ext.toList import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.toList import java.lang.ref.WeakReference import java.util.* import javax.inject.Inject @@ -24,7 +25,7 @@ import kotlin.coroutines.suspendCoroutine @Singleton class MangaLoaderContextImpl @Inject constructor( - override val httpClient: OkHttpClient, + @MangaHttpClient override val httpClient: OkHttpClient, override val cookieJar: MutableCookieJar, @ApplicationContext private val androidContext: Context, ) : MangaLoaderContext() { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaParser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/MangaParser.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaParser.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt index b90b10b52..8fab1b77c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.parser import androidx.annotation.AnyThread import org.koitharu.kotatsu.core.cache.ContentCache -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -22,6 +22,8 @@ interface MangaRepository { val sortOrders: Set + var defaultSortOrder: SortOrder + suspend fun getList(offset: Int, query: String): List suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index a99cd9acc..3df9988bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.SafeDeferred import org.koitharu.kotatsu.core.prefs.SourceSettings +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.config.ConfigKey @@ -25,8 +26,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.domain -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable class RemoteMangaRepository( private val parser: MangaParser, @@ -39,8 +39,8 @@ class RemoteMangaRepository( override val sortOrders: Set get() = parser.sortOrders - var defaultSortOrder: SortOrder? - get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull() + override var defaultSortOrder: SortOrder + get() = getConfig().defaultSortOrder ?: sortOrders.first() set(value) { getConfig().defaultSortOrder = value } @@ -101,7 +101,7 @@ class RemoteMangaRepository( } fun getAvailableMirrors(): List { - return parser.configKeyDomain.presetValues?.toList().orEmpty() + return parser.configKeyDomain.presetValues.toList() } private fun getConfig() = parser.config as SourceSettings diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt index be0d0dcfc..6af4218ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt @@ -86,7 +86,7 @@ class FaviconFetcher( if (!options.diskCachePolicy.readEnabled) { return null } - val snapshot = diskCache.value?.get(diskCacheKey) ?: return null + val snapshot = diskCache.value?.openSnapshot(diskCacheKey) ?: return null return SourceResult( source = snapshot.toImageSource(), mimeType = null, @@ -98,12 +98,12 @@ class FaviconFetcher( if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) { return null } - val editor = diskCache.value?.edit(diskCacheKey) ?: return null + val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null try { fileSystem.write(editor.data) { body.source().readAll(this) } - return editor.commitAndGet() + return editor.commitAndOpenSnapshot() } catch (e: Throwable) { try { editor.abort() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconUri.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconUri.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/favicon/FaviconUri.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconUri.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index dc13b7abb..b48123670 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -2,7 +2,9 @@ package org.koitharu.kotatsu.core.prefs import android.content.Context import android.content.SharedPreferences +import android.net.ConnectivityManager import android.net.Uri +import android.os.Build import android.provider.Settings import androidx.annotation.FloatRange import androidx.appcompat.app.AppCompatDelegate @@ -14,17 +16,18 @@ import dagger.hilt.android.qualifiers.ApplicationContext import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider +import org.koitharu.kotatsu.core.util.ext.connectivityManager +import org.koitharu.kotatsu.core.util.ext.filterToSet +import org.koitharu.kotatsu.core.util.ext.getEnumValue +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.putEnumValue +import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.shelf.domain.ShelfSection -import org.koitharu.kotatsu.utils.ext.connectivityManager -import org.koitharu.kotatsu.utils.ext.filterToSet -import org.koitharu.kotatsu.utils.ext.getEnumValue -import org.koitharu.kotatsu.utils.ext.observe -import org.koitharu.kotatsu.utils.ext.putEnumValue -import org.koitharu.kotatsu.utils.ext.toUriOrNull +import org.koitharu.kotatsu.shelf.domain.model.ShelfSection import java.io.File +import java.net.Proxy import java.util.Collections import java.util.EnumSet import java.util.Locale @@ -178,10 +181,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isUnstableUpdatesAllowed: Boolean get() = prefs.getBoolean(KEY_UPDATES_UNSTABLE, false) - fun isContentPrefetchEnabled(): Boolean { - val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER) - return policy.isNetworkAllowed(connectivityManager) - } + val isContentPrefetchEnabled: Boolean + get() { + if (isBackgroundNetworkRestricted()) { + return false + } + val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER) + return policy.isNetworkAllowed(connectivityManager) + } var sourcesOrder: List get() = prefs.getString(KEY_SOURCES_ORDER, null) @@ -270,12 +277,33 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isReaderSliderEnabled: Boolean get() = prefs.getBoolean(KEY_READER_SLIDER, true) + val isImagesProxyEnabled: Boolean + get() = prefs.getBoolean(KEY_IMAGES_PROXY, false) + val dnsOverHttps: DoHProvider get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE) val isSSLBypassEnabled: Boolean get() = prefs.getBoolean(KEY_SSL_BYPASS, false) + val proxyType: Proxy.Type + get() { + val raw = prefs.getString(KEY_PROXY_TYPE, null) ?: return Proxy.Type.DIRECT + return enumValues().find { it.name == raw } ?: Proxy.Type.DIRECT + } + + val proxyAddress: String? + get() = prefs.getString(KEY_PROXY_ADDRESS, null) + + val proxyPort: Int + get() = prefs.getString(KEY_PROXY_PORT, null)?.toIntOrNull() ?: 0 + + val proxyLogin: String? + get() = prefs.getString(KEY_PROXY_LOGIN, null)?.takeUnless { it.isEmpty() } + + val proxyPassword: String? + get() = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeUnless { it.isEmpty() } + var localListOrder: SortOrder get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST) set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } @@ -288,10 +316,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f) set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit { putFloat(KEY_READER_AUTOSCROLL_SPEED, value) } - fun isPagesPreloadEnabled(): Boolean { - val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED) - return policy.isNetworkAllowed(connectivityManager) - } + val isPagesPreloadEnabled: Boolean + get() { + if (isBackgroundNetworkRestricted()) { + return false + } + val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED) + return policy.isNetworkAllowed(connectivityManager) + } fun getMangaSources(includeHidden: Boolean): List { val list = remoteSources.toMutableList() @@ -329,6 +361,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { fun observe() = prefs.observe() + private fun isBackgroundNetworkRestricted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED + } else { + false + } + } + companion object { const val PAGE_SWITCH_TAPS = "taps" @@ -413,6 +453,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SSL_BYPASS = "ssl_bypass" const val KEY_READER_AUTOSCROLL_SPEED = "as_speed" const val KEY_MIRROR_SWITCHING = "mirror_switching" + const val KEY_PROXY = "proxy" + const val KEY_PROXY_TYPE = "proxy_type" + const val KEY_PROXY_ADDRESS = "proxy_address" + const val KEY_PROXY_PORT = "proxy_port" + const val KEY_PROXY_AUTH = "proxy_auth" + const val KEY_PROXY_LOGIN = "proxy_login" + const val KEY_PROXY_PASSWORD = "proxy_password" + const val KEY_IMAGES_PROXY = "images_proxy" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt index 606ae9d84..ed6d14f68 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt @@ -1,13 +1,11 @@ package org.koitharu.kotatsu.core.prefs -import androidx.lifecycle.liveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transform -import kotlin.coroutines.CoroutineContext fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow { var lastValue: T = valueProducer() @@ -23,25 +21,9 @@ fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> } } -fun AppSettings.observeAsLiveData( - context: CoroutineContext, - key: String, - valueProducer: AppSettings.() -> T, -) = liveData(context) { - emit(valueProducer()) - observe().collect { - if (it == key) { - val value = valueProducer() - if (value != latestValue) { - emit(value) - } - } - } -} - fun AppSettings.observeAsStateFlow( - key: String, scope: CoroutineScope, + key: String, valueProducer: AppSettings.() -> T, ): StateFlow = observe().transform { if (it == key) { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppWidgetConfig.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ColorScheme.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ColorScheme.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/ColorScheme.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ColorScheme.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ListMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ListMode.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/ListMode.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ListMode.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/NetworkPolicy.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/NetworkPolicy.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/NetworkPolicy.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/NetworkPolicy.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ScreenshotsPolicy.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ScreenshotsPolicy.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/ScreenshotsPolicy.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ScreenshotsPolicy.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index 1b3af7980..0080c0b1d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -2,13 +2,13 @@ package org.koitharu.kotatsu.core.prefs import android.content.Context import androidx.core.content.edit +import org.koitharu.kotatsu.core.util.ext.getEnumValue +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.ext.getEnumValue -import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty -import org.koitharu.kotatsu.utils.ext.putEnumValue private const val KEY_SORT_ORDER = "sort_order" diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt similarity index 53% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt index a667fec32..6809043ef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/AlertDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt @@ -1,8 +1,9 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.appcompat.app.AlertDialog @@ -12,18 +13,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder abstract class AlertDialogFragment : DialogFragment() { - private var viewBinding: B? = null + var viewBinding: B? = null + private set + @Deprecated("", ReplaceWith("requireViewBinding()")) protected val binding: B - get() = checkNotNull(viewBinding) + get() = requireViewBinding() final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val binding = onInflateView(layoutInflater, null) + val binding = onCreateViewBinding(layoutInflater, null) viewBinding = binding return MaterialAlertDialogBuilder(requireContext(), theme) .setView(binding.root) .run(::onBuildDialog) .create() + .also(::onDialogCreated) } final override fun onCreateView( @@ -32,6 +36,11 @@ abstract class AlertDialogFragment : DialogFragment() { savedInstanceState: Bundle?, ) = viewBinding?.root + final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + onViewBindingCreated(requireViewBinding(), savedInstanceState) + } + @CallSuper override fun onDestroyView() { viewBinding = null @@ -42,7 +51,14 @@ abstract class AlertDialogFragment : DialogFragment() { open fun onDialogCreated(dialog: AlertDialog) = Unit - protected fun bindingOrNull(): B? = viewBinding + @Deprecated("", ReplaceWith("viewBinding")) + protected fun bindingOrNull() = viewBinding + + fun requireViewBinding(): B = checkNotNull(viewBinding) { + "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." + } + + protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B - protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B + protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt index c9474a173..be9a10694 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.content.Intent import android.content.res.Configuration @@ -25,11 +25,11 @@ import androidx.viewbinding.ViewBinding import dagger.hilt.android.EntryPointAccessors import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate -import org.koitharu.kotatsu.base.ui.util.BaseActivityEntryPoint -import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver -import org.koitharu.kotatsu.utils.ext.getThemeColor +import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate +import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate +import org.koitharu.kotatsu.core.util.ext.getThemeColor @Suppress("LeakingThis") abstract class BaseActivity : @@ -38,14 +38,14 @@ abstract class BaseActivity : private var isAmoledTheme = false - protected lateinit var binding: B + lateinit var viewBinding: B private set @JvmField protected val exceptionResolver = ExceptionResolver(this) @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) + protected val insetsDelegate = WindowInsetsDelegate() @JvmField val actionModeDelegate = ActionModeDelegate() @@ -62,6 +62,7 @@ abstract class BaseActivity : super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) insetsDelegate.handleImeInsets = true + insetsDelegate.addInsetsListener(this) putDataToExtras(intent) } @@ -88,7 +89,7 @@ abstract class BaseActivity : } protected fun setContentView(binding: B) { - this.binding = binding + this.viewBinding = binding super.setContentView(binding.root) val toolbar = (binding.root.findViewById(R.id.toolbar) as? Toolbar) toolbar?.let(this::setSupportActionBar) @@ -131,7 +132,7 @@ abstract class BaseActivity : } else { ContextCompat.getColor(this, R.color.kotatsu_secondaryContainer) } - val insets = ViewCompat.getRootWindowInsets(binding.root) + val insets = ViewCompat.getRootWindowInsets(viewBinding.root) ?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return findViewById(androidx.appcompat.R.id.action_mode_bar).apply { setBackgroundColor(actionModeColor) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt index 6207ce83f..0cf8dfc1e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseBottomSheet.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.app.Dialog import android.os.Bundle @@ -13,17 +13,23 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog -import org.koitharu.kotatsu.utils.ext.findActivity -import org.koitharu.kotatsu.utils.ext.getDisplaySize +import org.koitharu.kotatsu.core.ui.dialog.AppBottomSheetDialog +import org.koitharu.kotatsu.core.util.ext.findActivity +import org.koitharu.kotatsu.core.util.ext.getDisplaySize import com.google.android.material.R as materialR +@Deprecated( + "Use BaseAdaptiveSheet", + replaceWith = ReplaceWith("BaseAdaptiveSheet", "org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet"), +) abstract class BaseBottomSheet : BottomSheetDialogFragment() { - private var viewBinding: B? = null + var viewBinding: B? = null + private set + @Deprecated("", ReplaceWith("requireViewBinding()")) protected val binding: B - get() = checkNotNull(viewBinding) + get() = requireViewBinding() protected val behavior: BottomSheetBehavior<*>? get() = (dialog as? BottomSheetDialog)?.behavior @@ -39,13 +45,14 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - val binding = onInflateView(inflater, container) + val binding = onCreateViewBinding(inflater, container) viewBinding = binding return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = requireViewBinding() // Enforce max width for tablets val width = resources.getDimensionPixelSize(R.dimen.bottom_sheet_width) if (width > 0) { @@ -55,6 +62,7 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { binding.root.context.findActivity()?.getDisplaySize()?.let { behavior?.peekHeight = (it.height() * 0.4).toInt() } + onViewBindingCreated(binding, savedInstanceState) } override fun onDestroyView() { @@ -75,7 +83,9 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { } } - protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B + protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B + + protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { val b = behavior ?: return @@ -89,4 +99,8 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { } b.isDraggable = !isLocked } + + fun requireViewBinding(): B = checkNotNull(viewBinding) { + "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt new file mode 100644 index 000000000..0809799b9 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt @@ -0,0 +1,68 @@ +package org.koitharu.kotatsu.core.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding +import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate + +@Suppress("LeakingThis") +abstract class BaseFragment : + Fragment(), + WindowInsetsDelegate.WindowInsetsListener { + + var viewBinding: B? = null + private set + + @Deprecated("", ReplaceWith("requireViewBinding()")) + protected val binding: B + get() = requireViewBinding() + + @JvmField + protected val exceptionResolver = ExceptionResolver(this) + + @JvmField + protected val insetsDelegate = WindowInsetsDelegate() + + protected val actionModeDelegate: ActionModeDelegate + get() = (requireActivity() as BaseActivity<*>).actionModeDelegate + + final override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = onCreateViewBinding(inflater, container) + viewBinding = binding + return binding.root + } + + final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + insetsDelegate.onViewCreated(view) + insetsDelegate.addInsetsListener(this) + onViewBindingCreated(requireViewBinding(), savedInstanceState) + } + + override fun onDestroyView() { + viewBinding = null + insetsDelegate.removeInsetsListener(this) + insetsDelegate.onDestroyView() + super.onDestroyView() + } + + fun requireViewBinding(): B = checkNotNull(viewBinding) { + "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." + } + + @Deprecated("", ReplaceWith("viewBinding")) + protected fun bindingOrNull() = viewBinding + + protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B + + protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt new file mode 100644 index 000000000..e5faecd51 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.core.ui + +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.viewbinding.ViewBinding +import org.koitharu.kotatsu.R + +abstract class BaseFullscreenActivity : + BaseActivity() { + + private lateinit var insetsControllerCompat: WindowInsetsControllerCompat + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + with(window) { + insetsControllerCompat = WindowInsetsControllerCompat(this, decorView) + statusBarColor = Color.TRANSPARENT + navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim) + } else { + Color.TRANSPARENT + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + showSystemUI() + } + + protected fun hideSystemUI() { + insetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()) + } + + protected fun showSystemUI() { + insetsControllerCompat.show(WindowInsetsCompat.Type.systemBars()) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt index 809944e8c..ce407ddca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.os.Bundle import android.view.View @@ -9,13 +9,13 @@ import androidx.core.view.updatePadding import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner -import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.settings.SettingsHeadersFragment +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.settings.SettingsActivity import javax.inject.Inject -@Suppress("LeakingThis") @AndroidEntryPoint abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(), @@ -26,27 +26,28 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : lateinit var settings: AppSettings @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) + protected val insetsDelegate = WindowInsetsDelegate() override val recyclerView: RecyclerView get() = listView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + view.setBackgroundColor(view.context.getThemeColor(android.R.attr.colorBackground)) listView.clipToPadding = false insetsDelegate.onViewCreated(view) + insetsDelegate.addInsetsListener(this) } override fun onDestroyView() { + insetsDelegate.removeInsetsListener(this) insetsDelegate.onDestroyView() super.onDestroyView() } override fun onResume() { super.onResume() - if (titleId != 0) { - setTitle(getString(titleId)) - } + setTitle(if (titleId != 0) getString(titleId) else null) } @CallSuper @@ -56,8 +57,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : ) } - protected fun setTitle(title: CharSequence) { - (parentFragment as? SettingsHeadersFragment)?.setTitle(title) - ?: activity?.setTitle(title) + protected fun setTitle(title: CharSequence?) { + (activity as? SettingsActivity)?.setSectionTitle(title) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseService.kt new file mode 100644 index 000000000..7a8f1463c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseService.kt @@ -0,0 +1,5 @@ +package org.koitharu.kotatsu.core.ui + +import androidx.lifecycle.LifecycleService + +abstract class BaseService : LifecycleService() diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt similarity index 56% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt index ac5f78b09..cdf31661b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseViewModel.kt @@ -1,6 +1,5 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui -import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CancellationException @@ -8,26 +7,34 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.EventFlow +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext abstract class BaseViewModel : ViewModel() { @JvmField - protected val loadingCounter = CountedBooleanLiveData() + protected val loadingCounter = MutableStateFlow(0) @JvmField - protected val errorEvent = SingleLiveEvent() + protected val errorEvent = MutableEventFlow() - val onError: LiveData + val onError: EventFlow get() = errorEvent - val isLoading: LiveData - get() = loadingCounter + val isLoading: StateFlow + get() = loadingCounter.map { it > 0 } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) protected fun launchJob( context: CoroutineContext = EmptyCoroutineContext, @@ -51,7 +58,11 @@ abstract class BaseViewModel : ViewModel() { private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> throwable.printStackTraceDebug() if (throwable !is CancellationException) { - errorEvent.postCall(throwable) + errorEvent.call(throwable) } } + + protected fun MutableStateFlow.increment() = update { it + 1 } + + protected fun MutableStateFlow.decrement() = update { it - 1 } } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt index 1fd56bd94..2d4ead31d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.content.Intent import androidx.lifecycle.lifecycleScope @@ -9,7 +9,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug abstract class CoroutineIntentService : BaseService() { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/DefaultActivityLifecycleCallbacks.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/DefaultActivityLifecycleCallbacks.kt index dd83e4dc7..f97db54ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/DefaultActivityLifecycleCallbacks.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/DefaultActivityLifecycleCallbacks.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AppBottomSheetDialog.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AppBottomSheetDialog.kt index 8b6da8d3d..f76e27d11 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AppBottomSheetDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.Context import android.graphics.Color @@ -26,4 +26,4 @@ class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(con } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/CheckBoxAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CheckBoxAlertDialog.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/CheckBoxAlertDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CheckBoxAlertDialog.kt index c452bd1ce..f246aba42 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/CheckBoxAlertDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CheckBoxAlertDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.Context import android.content.DialogInterface @@ -77,4 +77,4 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) fun create() = CheckBoxAlertDialog(delegate.create()) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/ErrorDetailsDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/ErrorDetailsDialog.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/core/ui/ErrorDetailsDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/ErrorDetailsDialog.kt index a9bb5eb8a..6f9d0f12c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/ErrorDetailsDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/ErrorDetailsDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.core.ui +package org.koitharu.kotatsu.core.ui.dialog import android.content.ClipData import android.content.ClipboardManager @@ -6,7 +6,6 @@ import android.content.Context import android.os.Bundle import android.text.method.LinkMovementMethod import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.text.HtmlCompat import androidx.core.text.htmlEncode @@ -14,12 +13,12 @@ import androidx.core.text.parseAsHtml import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.isReportable +import org.koitharu.kotatsu.core.util.ext.report +import org.koitharu.kotatsu.core.util.ext.requireSerializable +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.DialogErrorDetailsBinding -import org.koitharu.kotatsu.utils.ext.isReportable -import org.koitharu.kotatsu.utils.ext.report -import org.koitharu.kotatsu.utils.ext.requireSerializable -import org.koitharu.kotatsu.utils.ext.withArgs class ErrorDetailsDialog : AlertDialogFragment() { @@ -31,12 +30,12 @@ class ErrorDetailsDialog : AlertDialogFragment() { exception = args.requireSerializable(ARG_ERROR) } - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogErrorDetailsBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogErrorDetailsBinding { return DialogErrorDetailsBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) with(binding.textViewMessage) { movementMethod = LinkMovementMethod.getInstance() text = context.getString( diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RecyclerViewAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RecyclerViewAlertDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt index a2e28fd17..3199138e4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RecyclerViewAlertDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.Context import android.content.DialogInterface diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RememberSelectionDialogListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RememberSelectionDialogListener.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RememberSelectionDialogListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RememberSelectionDialogListener.kt index 7783a564b..e98e5d992 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/RememberSelectionDialogListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RememberSelectionDialogListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.DialogInterface @@ -10,4 +10,4 @@ class RememberSelectionDialogListener(initialValue: Int) : DialogInterface.OnCli override fun onClick(dialog: DialogInterface?, which: Int) { selection = which } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/StorageSelectDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/StorageSelectDialog.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/StorageSelectDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/StorageSelectDialog.kt index 58e353ca8..efc47ffde 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/StorageSelectDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/StorageSelectDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.Context import android.content.DialogInterface @@ -98,4 +98,4 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog) fun onStorageSelected(file: File) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TwoButtonsAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TwoButtonsAlertDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt index 12bc0d955..4d15077e1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TwoButtonsAlertDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.dialog +package org.koitharu.kotatsu.core.ui.dialog import android.content.Context import android.content.DialogInterface diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/CoilImageGetter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoilImageGetter.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/utils/image/CoilImageGetter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoilImageGetter.kt index 381d71d9f..7c5a5467f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/CoilImageGetter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoilImageGetter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.image +package org.koitharu.kotatsu.core.ui.image import android.content.Context import android.graphics.drawable.Drawable diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt index 69f61133f..43d662759 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.image +package org.koitharu.kotatsu.core.ui.image import android.view.View import android.view.View.OnLayoutChangeListener @@ -7,10 +7,10 @@ import android.widget.ImageView import coil.size.Dimension import coil.size.Size import coil.size.SizeResolver -import kotlin.coroutines.resume -import kotlin.math.roundToInt import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.math.roundToInt private const val ASPECT_RATIO_HEIGHT = 18f private const val ASPECT_RATIO_WIDTH = 13f @@ -80,4 +80,4 @@ class CoverSizeResolver( continuation.resume(size) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconFallbackDrawable.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconFallbackDrawable.kt index f6fdaa7df..ab065e004 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconFallbackDrawable.kt @@ -1,7 +1,12 @@ -package org.koitharu.kotatsu.utils.image +package org.koitharu.kotatsu.core.ui.image import android.content.Context -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.core.graphics.ColorUtils import com.google.android.material.color.MaterialColors diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/RegionBitmapDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/utils/image/RegionBitmapDecoder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt index 9736f6776..47d5461cb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/RegionBitmapDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.image +package org.koitharu.kotatsu.core.ui.image import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -13,7 +13,11 @@ import coil.decode.Decoder import coil.decode.ImageSource import coil.fetch.SourceResult import coil.request.Options -import coil.size.* +import coil.size.Dimension +import coil.size.Scale +import coil.size.Size +import coil.size.isOriginal +import coil.size.pxOrElse import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt index b44281f38..bc22724ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt @@ -1,8 +1,12 @@ -package org.koitharu.kotatsu.utils.image +package org.koitharu.kotatsu.core.ui.image import android.graphics.Bitmap import androidx.annotation.ColorInt -import androidx.core.graphics.* +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.get +import androidx.core.graphics.green +import androidx.core.graphics.red import coil.size.Size import coil.transform.Transformation import kotlin.math.abs @@ -104,4 +108,4 @@ class TrimTransformation( abs(a.blue - b.blue) <= tolerance && abs(a.alpha - b.alpha) <= tolerance } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/AdapterDelegateClickListenerAdapter.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/AdapterDelegateClickListenerAdapter.kt index 19d1d5661..a9e6e13ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/AdapterDelegateClickListenerAdapter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.view.View import android.view.View.OnClickListener diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BoundsScrollListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/BoundsScrollListener.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/BoundsScrollListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/BoundsScrollListener.kt index 11d65d7b3..9aa7cdf93 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/BoundsScrollListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/BoundsScrollListener.kt @@ -1,10 +1,12 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) : - RecyclerView.OnScrollListener() { +abstract class BoundsScrollListener( + @JvmField protected val offsetTop: Int, + @JvmField protected val offsetBottom: Int +) : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) @@ -24,9 +26,16 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs if (firstVisibleItemPosition <= offsetTop) { onScrolledToStart(recyclerView) } + onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount) } abstract fun onScrolledToStart(recyclerView: RecyclerView) abstract fun onScrolledToEnd(recyclerView: RecyclerView) + + protected open fun onPostScrolled( + recyclerView: RecyclerView, + firstVisibleItemPosition: Int, + visibleItemCount: Int + ) = Unit } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightGridLayoutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightGridLayoutManager.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightGridLayoutManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightGridLayoutManager.kt index fc6564beb..ddb94ce34 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightGridLayoutManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightGridLayoutManager.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.content.Context import android.util.AttributeSet @@ -34,4 +34,4 @@ class FitHeightGridLayoutManager : GridLayoutManager { super.layoutDecoratedWithMargins(child, left, top, right, bottom) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightLinearLayoutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightLinearLayoutManager.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightLinearLayoutManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightLinearLayoutManager.kt index 64e73198a..f4a36a227 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/FitHeightLinearLayoutManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/FitHeightLinearLayoutManager.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.content.Context import android.util.AttributeSet @@ -34,4 +34,4 @@ class FitHeightLinearLayoutManager : LinearLayoutManager { super.layoutDecoratedWithMargins(child, left, top, right, bottom) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt index 5cadc9c6f..e552e1098 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/ListSelectionController.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ListSelectionController.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.app.Activity import android.os.Bundle @@ -12,9 +12,9 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryOwner -import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import kotlin.coroutines.EmptyCoroutineContext private const val KEY_SELECTION = "selection" private const val PROVIDER_NAME = "selection_decoration" diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/NestedScrollStateHandle.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/NestedScrollStateHandle.kt index 80d5310d3..b4946ccb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/NestedScrollStateHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/NestedScrollStateHandle.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.os.Bundle import android.os.Parcelable diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnListItemClickListener.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnListItemClickListener.kt index e61f85bb0..e394740b9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnListItemClickListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnListItemClickListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.view.View diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnTipCloseListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnTipCloseListener.kt similarity index 59% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnTipCloseListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnTipCloseListener.kt index 9c9721eef..81078afee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/OnTipCloseListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/OnTipCloseListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list interface OnTipCloseListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/PaginationScrollListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/PaginationScrollListener.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/PaginationScrollListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/PaginationScrollListener.kt index 5681cae23..4f70dcd4d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/PaginationScrollListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/PaginationScrollListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import androidx.recyclerview.widget.RecyclerView @@ -15,4 +15,4 @@ class PaginationScrollListener(offset: Int, private val callback: Callback) : fun onScrolledToEnd() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/SectionedSelectionController.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/SectionedSelectionController.kt index d210c6991..066b4fa59 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/SectionedSelectionController.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.app.Activity import android.os.Bundle @@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryOwner import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration import kotlin.coroutines.EmptyCoroutineContext private const val PROVIDER_NAME = "selection_decoration_sectioned" diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractDividerItemDecoration.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractDividerItemDecoration.kt index 2d91e71c7..ca4bbec76 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractDividerItemDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractDividerItemDecoration.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.decor +package org.koitharu.kotatsu.core.ui.list.decor import android.annotation.SuppressLint import android.content.Context @@ -59,7 +59,7 @@ abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.It left, parent.paddingTop.toFloat(), right, - (parent.height - parent.paddingBottom).toFloat() + (parent.height - parent.paddingBottom).toFloat(), ) } else { left = 0f @@ -84,4 +84,4 @@ abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.It above: RecyclerView.ViewHolder, below: RecyclerView.ViewHolder, ): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt index 1974f6a5d..20e3aef78 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/AbstractSelectionItemDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/AbstractSelectionItemDecoration.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.decor +package org.koitharu.kotatsu.core.ui.list.decor import android.graphics.Canvas import android.graphics.Rect @@ -67,7 +67,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() { if (parent.clipToPadding) { canvas.clipRect( parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight, - parent.height - parent.paddingBottom + parent.height - parent.paddingBottom, ) } @@ -108,4 +108,4 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() { bounds: RectF, state: RecyclerView.State, ) = Unit -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SpacingItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/SpacingItemDecoration.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SpacingItemDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/SpacingItemDecoration.kt index 5b9fbde29..88f3593ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/SpacingItemDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/SpacingItemDecoration.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.decor +package org.koitharu.kotatsu.core.ui.list.decor import android.graphics.Rect import android.view.View diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/TypedSpacingItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/TypedSpacingItemDecoration.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/TypedSpacingItemDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/TypedSpacingItemDecoration.kt index 5662f026a..244936dbf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/decor/TypedSpacingItemDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/decor/TypedSpacingItemDecoration.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.decor +package org.koitharu.kotatsu.core.ui.list.decor import android.graphics.Rect import android.util.SparseIntArray @@ -13,7 +13,7 @@ class TypedSpacingItemDecoration( ) : RecyclerView.ItemDecoration() { private val mapping = SparseIntArray(spacingMapping.size) - + init { spacingMapping.forEach { (k, v) -> mapping[k] = v } } @@ -32,4 +32,4 @@ class TypedSpacingItemDecoration( } outRect.set(spacing, spacing, spacing, spacing) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/BubbleAnimator.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/BubbleAnimator.kt index 36b5e0e5f..359edfc05 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/BubbleAnimator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/BubbleAnimator.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.fastscroll +package org.koitharu.kotatsu.core.ui.list.fastscroll import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -8,9 +8,9 @@ import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import androidx.core.view.isInvisible import androidx.core.view.isVisible +import org.koitharu.kotatsu.core.util.ext.animatorDurationScale +import org.koitharu.kotatsu.core.util.ext.measureWidth import kotlin.math.hypot -import org.koitharu.kotatsu.utils.ext.animatorDurationScale -import org.koitharu.kotatsu.utils.ext.measureWidth class BubbleAnimator( private val bubble: View, diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScrollRecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScrollRecyclerView.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScrollRecyclerView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScrollRecyclerView.kt index 5a7c1274e..1a1590019 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScrollRecyclerView.kt @@ -1,9 +1,10 @@ -package org.koitharu.kotatsu.base.ui.list.fastscroll +package org.koitharu.kotatsu.core.ui.list.fastscroll import android.content.Context import android.util.AttributeSet import android.view.ViewGroup import androidx.annotation.AttrRes +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.R @@ -15,6 +16,12 @@ class FastScrollRecyclerView @JvmOverloads constructor( val fastScroller = FastScroller(context, attrs) + var isFastScrollerEnabled: Boolean = true + set(value) { + field = value + fastScroller.isVisible = value && isVisible + } + init { fastScroller.id = R.id.fast_scroller fastScroller.layoutParams = ViewGroup.LayoutParams( @@ -30,7 +37,7 @@ class FastScrollRecyclerView @JvmOverloads constructor( override fun setVisibility(visibility: Int) { super.setVisibility(visibility) - fastScroller.visibility = visibility + fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE } override fun onAttachedToWindow() { @@ -42,4 +49,4 @@ class FastScrollRecyclerView @JvmOverloads constructor( fastScroller.detachRecyclerView() super.onDetachedFromWindow() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt index e5cb94dd4..946992aea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/FastScroller.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.fastscroll +package org.koitharu.kotatsu.core.ui.list.fastscroll import android.annotation.SuppressLint import android.content.Context @@ -9,6 +9,7 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import android.widget.* import androidx.annotation.* @@ -22,9 +23,10 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.isLayoutReversed +import org.koitharu.kotatsu.core.util.ext.parents import org.koitharu.kotatsu.databinding.FastScrollerBinding -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.isLayoutReversed import kotlin.math.roundToInt import com.google.android.material.R as materialR @@ -56,6 +58,7 @@ class FastScroller @JvmOverloads constructor( private var bubbleHeight = 0 private var handleHeight = 0 private var viewHeight = 0 + private var offset = 0 private var hideScrollbar = true private var showBubble = true private var showBubbleAlways = false @@ -98,6 +101,7 @@ class FastScroller @JvmOverloads constructor( showScrollbar() if (showBubbleAlways && sectionIndexer != null) showBubble() } + RecyclerView.SCROLL_STATE_IDLE -> if (hideScrollbar && !binding.thumb.isSelected) { handler.postDelayed(scrollbarHider, SCROLLBAR_HIDE_DELAY) } @@ -113,6 +117,9 @@ class FastScroller @JvmOverloads constructor( return viewHeight * proportion } + val isScrollbarVisible: Boolean + get() = binding.scrollbar.isVisible + init { clipChildren = false orientation = HORIZONTAL @@ -136,6 +143,7 @@ class FastScroller @JvmOverloads constructor( bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL) val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize) binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + offset = getDimensionPixelOffset(R.styleable.FastScroller_scrollerOffset, offset) } setTrackColor(trackColor) @@ -162,7 +170,9 @@ class FastScroller @JvmOverloads constructor( when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { - if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false + if (!isScrollbarVisible || event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) { + return false + } requestDisallowInterceptTouchEvent(true) setHandleSelected(true) @@ -176,10 +186,12 @@ class FastScroller @JvmOverloads constructor( setYPositions() return true } + MotionEvent.ACTION_MOVE -> { setYPositions() return true } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { requestDisallowInterceptTouchEvent(false) setHandleSelected(false) @@ -245,27 +257,31 @@ class FastScroller @JvmOverloads constructor( layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply { height = 0 - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } } + is CoordinatorLayout -> layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply { height = LayoutParams.MATCH_PARENT anchorGravity = GravityCompat.END anchorId = recyclerViewId - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } + is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { height = LayoutParams.MATCH_PARENT gravity = GravityCompat.END - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } + is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply { height = 0 addRule(RelativeLayout.ALIGN_TOP, recyclerViewId) addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId) addRule(RelativeLayout.ALIGN_END, recyclerViewId) - setMargins(0, marginTop, 0, marginBottom) + setMargins(offset, marginTop, offset, marginBottom) } + else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") } @@ -287,10 +303,12 @@ class FastScroller @JvmOverloads constructor( if (parent is ViewGroup) { setLayoutParams(parent as ViewGroup) - } else if (recyclerView.parent is ViewGroup) { - val viewGroup = recyclerView.parent as ViewGroup - viewGroup.addView(this) - setLayoutParams(viewGroup) + } else { + val viewGroup = findValidParent(recyclerView) + if (viewGroup != null) { + viewGroup.addView(this) + setLayoutParams(viewGroup) + } } recyclerView.addOnScrollListener(scrollListener) @@ -504,6 +522,14 @@ class FastScroller @JvmOverloads constructor( return BubbleSize.values().getOrNull(ordinal) ?: defaultValue } + private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p -> + if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) { + p as ViewGroup + } else { + null + } + } + private val BubbleSize.textSize @Px get() = resources.getDimension(textSizeId) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/ScrollbarAnimator.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/ScrollbarAnimator.kt index 75298a802..1d9287b2d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/ScrollbarAnimator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/fastscroll/ScrollbarAnimator.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.fastscroll +package org.koitharu.kotatsu.core.ui.list.fastscroll import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -7,7 +7,7 @@ import android.view.ViewPropertyAnimator import androidx.core.view.isInvisible import androidx.core.view.isVisible import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.animatorDurationScale +import org.koitharu.kotatsu.core.util.ext.animatorDurationScale class ScrollbarAnimator( private val scrollbar: View, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt index a11f56e01..8e468b5ad 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/DateTimeAgo.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt @@ -1,10 +1,10 @@ -package org.koitharu.kotatsu.core.ui +package org.koitharu.kotatsu.core.ui.model import android.content.res.Resources import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.daysDiff +import org.koitharu.kotatsu.core.util.ext.format import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.daysDiff -import org.koitharu.kotatsu.utils.ext.format import java.util.Date sealed class DateTimeAgo : ListModel { @@ -107,9 +107,7 @@ sealed class DateTimeAgo : ListModel { other as Absolute - if (day != other.day) return false - - return true + return day == other.day } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/SortOrder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/SortOrder.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/core/ui/SortOrder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/SortOrder.kt index 92b9fd9ef..71e6034e6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/SortOrder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/SortOrder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.core.ui +package org.koitharu.kotatsu.core.ui.model import androidx.annotation.StringRes import org.koitharu.kotatsu.R @@ -12,4 +12,4 @@ val SortOrder.titleRes: Int SortOrder.RATING -> R.string.by_rating SortOrder.NEWEST -> R.string.newest SortOrder.ALPHABETICAL -> R.string.by_name - } \ No newline at end of file + } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt new file mode 100644 index 000000000..bedd65148 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetBehavior.kt @@ -0,0 +1,123 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.app.Dialog +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.sidesheet.SideSheetBehavior +import com.google.android.material.sidesheet.SideSheetCallback +import com.google.android.material.sidesheet.SideSheetDialog +import java.util.LinkedList + +sealed class AdaptiveSheetBehavior { + + @JvmField + protected val callbacks = LinkedList() + + abstract var state: Int + + abstract var isDraggable: Boolean + + open val isHideable: Boolean = true + + fun addCallback(callback: AdaptiveSheetCallback) { + callbacks.add(callback) + } + + fun removeCallback(callback: AdaptiveSheetCallback) { + callbacks.remove(callback) + } + + class Bottom( + private val delegate: BottomSheetBehavior<*>, + ) : AdaptiveSheetBehavior() { + + override var state: Int + get() = delegate.state + set(value) { + delegate.state = value + } + + override var isDraggable: Boolean + get() = delegate.isDraggable + set(value) { + delegate.isDraggable = value + } + + override val isHideable: Boolean + get() = delegate.isHideable + + var isFitToContents: Boolean + get() = delegate.isFitToContents + set(value) { + delegate.isFitToContents = value + } + + init { + delegate.addBottomSheetCallback( + object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + callbacks.forEach { it.onStateChanged(bottomSheet, newState) } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + callbacks.forEach { it.onSlide(bottomSheet, slideOffset) } + } + }, + ) + } + } + + class Side( + private val delegate: SideSheetBehavior<*>, + ) : AdaptiveSheetBehavior() { + + override var state: Int + get() = delegate.state + set(value) { + delegate.state = value + } + + override var isDraggable: Boolean + get() = delegate.isDraggable + set(value) { + delegate.isDraggable = value + } + + init { + delegate.addCallback( + object : SideSheetCallback() { + override fun onStateChanged(sheet: View, newState: Int) { + callbacks.forEach { it.onStateChanged(sheet, newState) } + } + + override fun onSlide(sheet: View, slideOffset: Float) { + callbacks.forEach { it.onSlide(sheet, slideOffset) } + } + }, + ) + } + } + + companion object { + + const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED + const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING + const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING + const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN + + fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) { + is BottomSheetDialog -> Bottom(dialog.behavior) + is SideSheetDialog -> Side(dialog.behavior) + else -> null + } + + fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) { + is BottomSheetBehavior<*> -> Bottom(behavior) + is SideSheetBehavior<*> -> Side(behavior) + else -> null + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt new file mode 100644 index 000000000..9abaebbc1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetCallback.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.view.View + +interface AdaptiveSheetCallback { + + /** + * Called when the sheet changes its state. + * + * @param sheet The sheet view. + * @param newState The new state. + */ + fun onStateChanged(sheet: View, newState: Int) + + /** + * Called when the sheet is being dragged. + * + * @param sheet The sheet view. + * @param slideOffset The new offset of this sheet. + */ + fun onSlide(sheet: View, slideOffset: Float) = Unit +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt new file mode 100644 index 000000000..0e062a844 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/AdaptiveSheetHeaderBar.kt @@ -0,0 +1,94 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.AttrRes +import androidx.annotation.StringRes +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.withStyledAttributes +import androidx.core.view.isGone +import androidx.core.view.isVisible +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.parents +import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding + +class AdaptiveSheetHeaderBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback { + + private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this) + private var sheetBehavior: AdaptiveSheetBehavior? = null + + var title: CharSequence? + get() = binding.shTextViewTitle.text + set(value) { + binding.shTextViewTitle.text = value + } + + val isTitleVisible: Boolean + get() = binding.shLayoutSidesheet.isVisible + + init { + orientation = VERTICAL + binding.shButtonClose.setOnClickListener { dismissSheet() } + context.withStyledAttributes( + attrs, + R.styleable.AdaptiveSheetHeaderBar, defStyleAttr, + ) { + title = getText(R.styleable.AdaptiveSheetHeaderBar_title) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (isInEditMode) { + val isTabled = resources.getBoolean(R.bool.is_tablet) + binding.shDragHandle.isGone = isTabled + binding.shLayoutSidesheet.isVisible = isTabled + } else { + setBottomSheetBehavior(findParentSheetBehavior()) + } + } + + override fun onDetachedFromWindow() { + setBottomSheetBehavior(null) + super.onDetachedFromWindow() + } + + override fun onStateChanged(sheet: View, newState: Int) { + + } + + fun setTitle(@StringRes resId: Int) { + binding.shTextViewTitle.setText(resId) + } + + private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) { + binding.shDragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom + binding.shLayoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side + sheetBehavior?.removeCallback(this) + sheetBehavior = behavior + behavior?.addCallback(this) + } + + private fun dismissSheet() { + sheetBehavior?.state = AdaptiveSheetBehavior.STATE_HIDDEN + } + + private fun findParentSheetBehavior(): AdaptiveSheetBehavior? { + for (p in parents) { + val layoutParams = (p as? View)?.layoutParams + if (layoutParams is CoordinatorLayout.LayoutParams) { + AdaptiveSheetBehavior.from(layoutParams)?.let { + return it + } + } + } + return null + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt new file mode 100644 index 000000000..d9941765d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -0,0 +1,174 @@ +package org.koitharu.kotatsu.core.ui.sheet + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.activity.OnBackPressedDispatcher +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.view.updateLayoutParams +import androidx.viewbinding.ViewBinding +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.sidesheet.SideSheetDialog +import org.koitharu.kotatsu.R +import com.google.android.material.R as materialR + +abstract class BaseAdaptiveSheet : AppCompatDialogFragment() { + + private var waitingForDismissAllowingStateLoss = false + private var isFitToContentsDisabled = false + + var viewBinding: B? = null + private set + + @Deprecated("", ReplaceWith("requireViewBinding()")) + protected val binding: B + get() = requireViewBinding() + + protected val behavior: AdaptiveSheetBehavior? + get() = AdaptiveSheetBehavior.from(dialog) + + val isExpanded: Boolean + get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED + + val onBackPressedDispatcher: OnBackPressedDispatcher + get() = requireComponentDialog().onBackPressedDispatcher + + final override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + val binding = onCreateViewBinding(inflater, container) + viewBinding = binding + return binding.root + } + + final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val binding = requireViewBinding() + onViewBindingCreated(binding, savedInstanceState) + } + + override fun onDestroyView() { + viewBinding = null + super.onDestroyView() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + return if (context.resources.getBoolean(R.bool.is_tablet)) { + SideSheetDialog(context, theme) + } else { + BottomSheetDialog(context, theme) + } + } + + fun addSheetCallback(callback: AdaptiveSheetCallback) { + val b = behavior ?: return + b.addCallback(callback) + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) + ?: dialog?.findViewById(materialR.id.coordinator) + if (rootView != null) { + callback.onStateChanged(rootView, b.state) + } + } + + protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B + + protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit + + protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { + val b = behavior ?: return + if (isExpanded) { + b.state = BottomSheetBehavior.STATE_EXPANDED + } + if (b is AdaptiveSheetBehavior.Bottom) { + b.isFitToContents = !isFitToContentsDisabled && !isExpanded + val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet) + rootView?.updateLayoutParams { + height = if (isFitToContentsDisabled || isExpanded) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } + } + } + b.isDraggable = !isLocked + } + + protected fun disableFitToContents() { + isFitToContentsDisabled = true + val b = behavior as? AdaptiveSheetBehavior.Bottom ?: return + b.isFitToContents = false + dialog?.findViewById(materialR.id.design_bottom_sheet)?.updateLayoutParams { + height = LayoutParams.MATCH_PARENT + } + } + + fun requireViewBinding(): B = checkNotNull(viewBinding) { + "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." + } + + override fun dismiss() { + if (!tryDismissWithAnimation(false)) { + super.dismiss() + } + } + + override fun dismissAllowingStateLoss() { + if (!tryDismissWithAnimation(true)) { + super.dismissAllowingStateLoss() + } + } + + /** + * Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible, + * false otherwise. + */ + private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean { + val shouldDismissWithAnimation = when (val dialog = dialog) { + is BottomSheetDialog -> dialog.dismissWithAnimation + is SideSheetDialog -> dialog.isDismissWithSheetAnimationEnabled + else -> false + } + val behavior = behavior ?: return false + return if (shouldDismissWithAnimation && behavior.isHideable) { + dismissWithAnimation(behavior, allowingStateLoss) + true + } else { + false + } + } + + private fun dismissWithAnimation(behavior: AdaptiveSheetBehavior, allowingStateLoss: Boolean) { + waitingForDismissAllowingStateLoss = allowingStateLoss + if (behavior.state == AdaptiveSheetBehavior.STATE_HIDDEN) { + dismissAfterAnimation() + } else { + behavior.addCallback(SheetDismissCallback()) + behavior.state = AdaptiveSheetBehavior.STATE_HIDDEN + } + } + + private fun dismissAfterAnimation() { + if (waitingForDismissAllowingStateLoss) { + super.dismissAllowingStateLoss() + } else { + super.dismiss() + } + } + + private inner class SheetDismissCallback : AdaptiveSheetCallback { + override fun onStateChanged(sheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + dismissAfterAnimation() + } + } + + override fun onSlide(sheet: View, slideOffset: Float) {} + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt index feed3fc6a..2296aef53 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import androidx.activity.OnBackPressedCallback import androidx.appcompat.view.ActionMode @@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) { get() = activeActionMode != null override fun handleOnBackPressed() { - activeActionMode?.finish() + finishActionMode() } fun onSupportActionModeStarted(mode: ActionMode) { @@ -45,6 +45,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) { owner.lifecycle.addObserver(ListenerLifecycleObserver(listener)) } + fun finishActionMode() { + activeActionMode?.finish() + } + private inner class ListenerLifecycleObserver( private val listener: ActionModeListener, ) : DefaultLifecycleObserver { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeListener.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeListener.kt index 0c87ff612..fde599ede 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActionModeListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import androidx.appcompat.view.ActionMode @@ -7,4 +7,4 @@ interface ActionModeListener { fun onActionModeStarted(mode: ActionMode) fun onActionModeFinished(mode: ActionMode) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActivityRecreationHandle.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActivityRecreationHandle.kt index 036515bbe..46c1d0f9e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ActivityRecreationHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActivityRecreationHandle.kt @@ -1,9 +1,9 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.app.Activity import android.os.Bundle import androidx.core.app.ActivityCompat -import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks +import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks import java.util.WeakHashMap import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/BaseActivityEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/BaseActivityEntryPoint.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt index 66b1a588c..309883319 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/BaseActivityEntryPoint.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/BaseActivityEntryPoint.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import dagger.hilt.EntryPoint import dagger.hilt.InstallIn diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CollapseActionViewCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/CollapseActionViewCallback.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/CollapseActionViewCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/CollapseActionViewCallback.kt index 5d9058de1..b417e40e3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/CollapseActionViewCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/CollapseActionViewCallback.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.view.MenuItem import android.view.MenuItem.OnActionExpandListener diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/DefaultTextWatcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/DefaultTextWatcher.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/DefaultTextWatcher.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/DefaultTextWatcher.kt index a382f488c..999dd6641 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/DefaultTextWatcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/DefaultTextWatcher.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.text.Editable import android.text.TextWatcher diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/RecyclerViewOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/RecyclerViewOwner.kt similarity index 72% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/RecyclerViewOwner.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/RecyclerViewOwner.kt index 9b0976d51..f34963f15 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/RecyclerViewOwner.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/RecyclerViewOwner.kt @@ -1,8 +1,8 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import androidx.recyclerview.widget.RecyclerView interface RecyclerViewOwner { val recyclerView: RecyclerView -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleAction.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleAction.kt similarity index 56% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleAction.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleAction.kt index 57bb80a78..f9fea6652 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleAction.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleAction.kt @@ -1,9 +1,8 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import androidx.annotation.StringRes -import org.koitharu.kotatsu.base.domain.ReversibleHandle class ReversibleAction( @StringRes val stringResId: Int, val handle: ReversibleHandle?, -) \ No newline at end of file +) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleActionObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleActionObserver.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleActionObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleActionObserver.kt index ba685c4f4..b66e64cbb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ReversibleActionObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleActionObserver.kt @@ -1,19 +1,15 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.view.View -import androidx.lifecycle.Observer import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.flow.FlowCollector import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.reverseAsync class ReversibleActionObserver( private val snackbarHost: View, -) : Observer { +) : FlowCollector { - override fun onChanged(value: ReversibleAction?) { - if (value == null) { - return - } + override suspend fun emit(value: ReversibleAction) { val handle = value.handle val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG val snackbar = Snackbar.make(snackbarHost, value.stringResId, length) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleHandle.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleHandle.kt index f34c99e69..6095da3c1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ReversibleHandle.kt @@ -1,12 +1,12 @@ -package org.koitharu.kotatsu.base.domain +package org.koitharu.kotatsu.core.ui.util import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug fun interface ReversibleHandle { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ShrinkOnScrollBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ShrinkOnScrollBehavior.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/ShrinkOnScrollBehavior.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ShrinkOnScrollBehavior.kt index c22585755..8d648ec3a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/ShrinkOnScrollBehavior.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ShrinkOnScrollBehavior.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/SpanSizeResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/SpanSizeResolver.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/SpanSizeResolver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/SpanSizeResolver.kt index 71e5dc398..9c0e07f91 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/SpanSizeResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/SpanSizeResolver.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.view.View import androidx.annotation.Px diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/StatusBarDimHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/StatusBarDimHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt index 7a8bf28d4..b58f36ae1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/StatusBarDimHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/StatusBarDimHelper.kt @@ -1,11 +1,11 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.animation.ValueAnimator import android.view.animation.AccelerateDecelerateInterpolator -import com.google.android.material.R as materialR import com.google.android.material.appbar.AppBarLayout import com.google.android.material.shape.MaterialShapeDrawable -import org.koitharu.kotatsu.utils.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import com.google.android.material.R as materialR class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt similarity index 74% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt index ff80acbbb..a5e8c2d43 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt @@ -1,14 +1,13 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.view.View import androidx.core.graphics.Insets import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import java.util.LinkedList -class WindowInsetsDelegate( - private val listener: WindowInsetsListener, -) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener { +class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeListener { @JvmField var handleImeInsets: Boolean = false @@ -16,6 +15,7 @@ class WindowInsetsDelegate( @JvmField var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null + private val listeners = LinkedList() private var lastInsets: Insets? = null override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { @@ -29,7 +29,7 @@ class WindowInsetsDelegate( handledInsets.getInsets(WindowInsetsCompat.Type.systemBars()) } if (newInsets != lastInsets) { - listener.onWindowInsetsChanged(newInsets) + listeners.forEach { it.onWindowInsetsChanged(newInsets) } lastInsets = newInsets } return handledInsets @@ -52,6 +52,15 @@ class WindowInsetsDelegate( } } + fun addInsetsListener(listener: WindowInsetsListener) { + listeners.add(listener) + lastInsets?.let { listener.onWindowInsetsChanged(it) } + } + + fun removeInsetsListener(listener: WindowInsetsListener) { + listeners.remove(listener) + } + fun onViewCreated(view: View) { ViewCompat.setOnApplyWindowInsetsListener(view, this) view.addOnLayoutChangeListener(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt index 80b5749bf..1fa1dcf37 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/BottomSheetHeaderBar.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.animation.LayoutTransition import android.content.Context @@ -21,15 +21,16 @@ import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomsheet.BottomSheetBehavior import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.getThemeDrawable +import org.koitharu.kotatsu.core.util.ext.parents import org.koitharu.kotatsu.databinding.LayoutSheetHeaderBinding -import org.koitharu.kotatsu.utils.ext.getAnimationDuration -import org.koitharu.kotatsu.utils.ext.getThemeDrawable -import org.koitharu.kotatsu.utils.ext.parents import java.util.* import com.google.android.material.R as materialR private const val THROTTLE_DELAY = 200L +@Deprecated("") class BottomSheetHeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -70,6 +71,9 @@ class BottomSheetHeaderBar @JvmOverloads constructor( binding.toolbar.subtitle = value } + val isExpanded: Boolean + get() = binding.dragHandle.isGone + init { setBackgroundResource(R.drawable.sheet_toolbar_background) layoutTransition = LayoutTransition().apply { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageView.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageView.kt index 2d18292cc..b872917c0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CheckableImageView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.content.Context import android.os.Parcel @@ -101,4 +101,4 @@ class CheckableImageView @JvmOverloads constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index 88398cbd0..c2c526e91 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.annotation.SuppressLint import android.content.Context @@ -6,6 +6,7 @@ import android.content.res.ColorStateList import android.util.AttributeSet import android.view.View.OnClickListener import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.content.res.getColorStateListOrThrow import androidx.core.view.children @@ -13,7 +14,7 @@ import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.castOrNull +import org.koitharu.kotatsu.core.util.ext.castOrNull import com.google.android.material.R as materialR class ChipsView @JvmOverloads constructor( @@ -101,6 +102,13 @@ class ChipsView @JvmOverloads constructor( chip.setTextColor(tint ?: defaultChipTextColor) chip.isClickable = onChipClickListener != null || model.isCheckable chip.isCheckable = model.isCheckable + if (model.icon == 0) { + chip.chipIcon = null + chip.isChipIconVisible = false + } else { + chip.setChipIconResource(model.icon) + chip.isChipIconVisible = true + } chip.isChecked = model.isChecked chip.tag = model.data } @@ -134,6 +142,7 @@ class ChipsView @JvmOverloads constructor( class ChipModel( @ColorRes val tint: Int, val title: CharSequence, + @DrawableRes val icon: Int, val isCheckable: Boolean, val isChecked: Boolean, val data: Any? = null, @@ -147,16 +156,16 @@ class ChipsView @JvmOverloads constructor( if (tint != other.tint) return false if (title != other.title) return false + if (icon != other.icon) return false if (isCheckable != other.isCheckable) return false if (isChecked != other.isChecked) return false - if (data != other.data) return false - - return true + return data == other.data } override fun hashCode(): Int { var result = tint.hashCode() result = 31 * result + title.hashCode() + result = 31 * result + icon.hashCode() result = 31 * result + isCheckable.hashCode() result = 31 * result + isChecked.hashCode() result = 31 * result + (data?.hashCode() ?: 0) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CoverImageView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CoverImageView.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CoverImageView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CoverImageView.kt index 3a52eb237..9bcd4ca60 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/CoverImageView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CoverImageView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.content.Context import android.util.AttributeSet @@ -40,4 +40,4 @@ class CoverImageView @JvmOverloads constructor( } setMeasuredDimension(desiredWidth, desiredHeight) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/HideBottomNavigationOnScrollBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/HideBottomNavigationOnScrollBehavior.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/HideBottomNavigationOnScrollBehavior.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/HideBottomNavigationOnScrollBehavior.kt index b1742420a..629ffcd12 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/HideBottomNavigationOnScrollBehavior.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/HideBottomNavigationOnScrollBehavior.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.animation.ValueAnimator import android.content.Context @@ -10,8 +10,8 @@ import androidx.core.view.ViewCompat import com.google.android.material.appbar.AppBarLayout import com.google.android.material.bottomnavigation.BottomNavigationView import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.getAnimationDuration -import org.koitharu.kotatsu.utils.ext.measureHeight +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.measureHeight class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor( context: Context? = null, diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ListItemTextView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ListItemTextView.kt index e51509920..71d9314f7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ListItemTextView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.annotation.SuppressLint import android.content.Context @@ -19,7 +19,7 @@ import com.google.android.material.ripple.RippleUtils import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.resolveDp @SuppressLint("RestrictedApi") class ListItemTextView @JvmOverloads constructor( diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SegmentedBarView.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SegmentedBarView.kt index 1125b7839..39591490a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SegmentedBarView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.animation.Animator import android.animation.ValueAnimator @@ -12,11 +12,11 @@ import android.view.ViewOutlineProvider import android.view.animation.DecelerateInterpolator import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.parsers.util.replaceWith -import org.koitharu.kotatsu.utils.ext.getAnimationDuration -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled -import org.koitharu.kotatsu.utils.ext.resolveDp import com.google.android.material.R as materialR class SegmentedBarView @JvmOverloads constructor( @@ -135,9 +135,7 @@ class SegmentedBarView @JvmOverloads constructor( other as Segment if (percent != other.percent) return false - if (color != other.color) return false - - return true + return color == other.color } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SelectableTextView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SelectableTextView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt index e931853f0..32cb29875 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SelectableTextView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.content.Context import android.text.Selection @@ -26,4 +26,4 @@ class SelectableTextView @JvmOverloads constructor( Selection.setSelection(spannableText, text.length) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ShapeView.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ShapeView.kt index 5ec934c1e..32b7f47dd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ShapeView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ShapeView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.annotation.SuppressLint import android.content.Context diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt index 3e9e7b55d..3a95fa398 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SlidingBottomNavigationView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -14,10 +14,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.customview.view.AbsSavedState import androidx.interpolator.view.animation.FastOutLinearInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator -import com.google.android.material.R as materialR import com.google.android.material.bottomnavigation.BottomNavigationView -import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale -import org.koitharu.kotatsu.utils.ext.measureHeight +import org.koitharu.kotatsu.core.util.ext.applySystemAnimatorScale +import org.koitharu.kotatsu.core.util.ext.measureHeight +import com.google.android.material.R as materialR private const val STATE_DOWN = 1 private const val STATE_UP = 2 @@ -92,6 +92,14 @@ class SlidingBottomNavigationView @JvmOverloads constructor( ) } + fun showOrHide(show: Boolean) { + if (show) { + show() + } else { + hide() + } + } + private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) { currentAnimator = animate() .translationY(targetY) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/TwoLinesItemView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/TwoLinesItemView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt index f7f8d44e1..37058bac2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/TwoLinesItemView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.annotation.SuppressLint import android.content.Context @@ -23,8 +23,8 @@ import com.google.android.material.ripple.RippleUtils import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding -import org.koitharu.kotatsu.utils.ext.resolveDp @SuppressLint("RestrictedApi") class TwoLinesItemView @JvmOverloads constructor( diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/WindowInsetHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/WindowInsetHolder.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/WindowInsetHolder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/WindowInsetHolder.kt index 3279dfc06..57870cf19 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/WindowInsetHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/WindowInsetHolder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.widgets +package org.koitharu.kotatsu.core.ui.widgets import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/AlphanumComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AlphanumComparator.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/AlphanumComparator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/AlphanumComparator.kt index cee0626c0..46867633e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/AlphanumComparator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AlphanumComparator.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util class AlphanumComparator : Comparator { @@ -60,4 +60,4 @@ class AlphanumComparator : Comparator { } return chunk.toString() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/BufferedObserver.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/BufferedObserver.kt index bc806ec7a..ccebce668 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/BufferedObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/BufferedObserver.kt @@ -1,6 +1,6 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util fun interface BufferedObserver { fun onChanged(t: T, previous: T?) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/CancellableSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/utils/CancellableSource.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt index 9830d86b6..d06daa6ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/CancellableSource.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex.kt index 99f69e11a..9c9c9f0d3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeMutex.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import androidx.collection.ArrayMap import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeRunnable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeRunnable.kt new file mode 100644 index 000000000..fe56ffc3c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CompositeRunnable.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.util + +class CompositeRunnable( + private val children: List, +) : Runnable, Collection by children { + + override fun run() { + for (child in children) { + child.run() + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/EditTextValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/EditTextValidator.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/EditTextValidator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/EditTextValidator.kt index 37ca77618..3554fc194 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/EditTextValidator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/EditTextValidator.kt @@ -1,11 +1,11 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.content.Context import android.text.Editable import android.text.TextWatcher import android.widget.EditText import androidx.annotation.CallSuper -import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import java.lang.ref.WeakReference abstract class EditTextValidator : TextWatcher { @@ -51,4 +51,4 @@ abstract class EditTextValidator : TextWatcher { class Failed(val message: CharSequence) : ValidationResult() } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Event.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Event.kt new file mode 100644 index 000000000..1fd768af1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Event.kt @@ -0,0 +1,36 @@ +package org.koitharu.kotatsu.core.util + +import kotlinx.coroutines.flow.FlowCollector + +class Event( + private val data: T, +) { + private var isConsumed = false + + suspend fun consume(collector: FlowCollector) { + if (!isConsumed) { + collector.emit(data) + isConsumed = true + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Event<*> + + if (data != other.data) return false + return isConsumed == other.isConsumed + } + + override fun hashCode(): Int { + var result = data?.hashCode() ?: 0 + result = 31 * result + isConsumed.hashCode() + return result + } + + override fun toString(): String { + return "Event(data=$data, isConsumed=$isConsumed)" + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/FileSize.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/FileSize.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/utils/FileSize.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/FileSize.kt index cb558edfe..6325c3dec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/FileSize.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/FileSize.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.content.Context import org.koitharu.kotatsu.R @@ -22,8 +22,8 @@ enum class FileSize(private val multiplier: Int) { return buildString { append( DecimalFormat("#,##0.#").format( - bytes / 1024.0.pow(digitGroups.toDouble()) - ) + bytes / 1024.0.pow(digitGroups.toDouble()), + ), ) val unit = units.getOrNull(digitGroups) if (unit != null) { @@ -32,4 +32,4 @@ enum class FileSize(private val multiplier: Int) { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GoneOnInvisibleListener.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/GoneOnInvisibleListener.kt index 46de769c6..25ab3717f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/GoneOnInvisibleListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GoneOnInvisibleListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.view.View import android.view.ViewTreeObserver diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt index 9605fb93b..6608c719e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/GridTouchHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.content.Context import android.view.GestureDetector @@ -44,6 +44,7 @@ class GridTouchHelper( else -> return false } } + 2 -> AREA_RIGHT else -> return false }, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/IdlingDetector.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/IdlingDetector.kt index 0501a3da6..d9cbedfdc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/IdlingDetector.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/IdlingDetector.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.os.Handler import android.os.Looper diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/IncognitoModeIndicator.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/IncognitoModeIndicator.kt index 8ca72f3eb..7dc2ee1ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/IncognitoModeIndicator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/IncognitoModeIndicator.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.app.Activity import android.os.Bundle @@ -11,10 +11,10 @@ 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 org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks +import org.koitharu.kotatsu.core.util.ext.getThemeColor import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MediatorStateFlow.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/MediatorStateFlow.kt index 0f4fda663..7bee7ffc2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/MediatorStateFlow.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/RecyclerViewScrollCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/RecyclerViewScrollCallback.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/RecyclerViewScrollCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/RecyclerViewScrollCallback.kt index 075126db2..f8815ae6e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/RecyclerViewScrollCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/RecyclerViewScrollCallback.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import androidx.annotation.Px import androidx.recyclerview.widget.LinearLayoutManager @@ -23,4 +23,4 @@ class RecyclerViewScrollCallback( lm.scrollToPosition(position) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/RetainedLifecycleCoroutineScope.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/RetainedLifecycleCoroutineScope.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/utils/RetainedLifecycleCoroutineScope.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/RetainedLifecycleCoroutineScope.kt index 66a232922..b14e842c7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/RetainedLifecycleCoroutineScope.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/RetainedLifecycleCoroutineScope.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import dagger.hilt.android.lifecycle.RetainedLifecycle import kotlinx.coroutines.CoroutineScope @@ -8,7 +8,7 @@ import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext class RetainedLifecycleCoroutineScope( - private val lifecycle: RetainedLifecycle, + val lifecycle: RetainedLifecycle, ) : CoroutineScope, RetainedLifecycle.OnClearedListener { override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ScreenOrientationHelper.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ScreenOrientationHelper.kt index 4cecbd2a8..8f062e247 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ScreenOrientationHelper.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.app.Activity import android.content.pm.ActivityInfo @@ -18,7 +18,7 @@ class ScreenOrientationHelper(private val activity: Activity) { get() = Settings.System.getInt( activity.contentResolver, Settings.System.ACCELEROMETER_ROTATION, - 0 + 0, ) == 1 var isLandscape: Boolean @@ -42,7 +42,7 @@ class ScreenOrientationHelper(private val activity: Activity) { } } activity.contentResolver.registerContentObserver( - Settings.System.CONTENT_URI, true, observer + Settings.System.CONTENT_URI, true, observer, ) awaitClose { activity.contentResolver.unregisterContentObserver(observer) @@ -50,4 +50,4 @@ class ScreenOrientationHelper(private val activity: Activity) { }.onStart { emit(isAutoRotationEnabled) }.distinctUntilChanged() -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt index 3bdbc5ce3..57d5e7c80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ShareHelper.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.content.Context import android.net.Uri diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt new file mode 100644 index 000000000..c55aaa121 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.util + +import android.app.Activity + +class TaggedActivityResult( + val tag: String, + val result: Int, +) { + + val isSuccess: Boolean + get() = result == Activity.RESULT_OK +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/Throttler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/utils/Throttler.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt index b026cf15e..5748c79bb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/Throttler.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.os.SystemClock diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ViewBadge.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/ViewBadge.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt index 90f7a94d7..e8aa4263d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ViewBadge.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ViewBadge.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.view.View import androidx.annotation.OptIn diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/WorkManagerHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/WorkManagerHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt index 0b768d3f5..95e7aaa4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/WorkManagerHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.annotation.SuppressLint import androidx.work.WorkInfo diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt index b0c426cc2..533c407a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.annotation.SuppressLint import android.content.Context @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope /** * Workaround for issue diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index 71d439811..463a16ec5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.app.Activity import android.app.ActivityManager @@ -16,6 +16,7 @@ import android.database.SQLException import android.graphics.Color import android.net.Uri import android.os.Build +import android.os.Bundle import android.provider.Settings import android.view.View import android.view.ViewPropertyAnimator @@ -42,6 +43,7 @@ import org.json.JSONException import org.jsoup.internal.StringUtil.StringJoiner import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import kotlin.math.roundToLong @@ -135,8 +137,8 @@ fun Context.getAnimationDuration(@IntegerRes resId: Int): Long { return (resources.getInteger(resId) * animatorDurationScale).roundToLong() } -fun isLowRamDevice(context: Context): Boolean { - return context.activityManager?.isLowRamDevice ?: false +fun Context.isLowRamDevice(): Boolean { + return activityManager?.isLowRamDevice ?: false } val Context.ramAvailable: Long @@ -146,13 +148,17 @@ val Context.ramAvailable: Long return result.availMem } -fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation( - view, - 0, - 0, - view.width, - view.height, -) +fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) { + ActivityOptions.makeScaleUpAnimation( + view, + 0, + 0, + view.width, + view.height, + ).toBundle() +} else { + null +} fun Resources.getLocalesConfig(): LocaleListCompat { val tagsList = StringJoiner(",") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/BottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/BottomSheet.kt new file mode 100644 index 000000000..faba08d87 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/BottomSheet.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.core.util.ext + +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback + +fun BottomSheetBehavior<*>.doOnExpansionsChanged(callback: (isExpanded: Boolean) -> Unit) { + var isExpended = state == BottomSheetBehavior.STATE_EXPANDED + callback(isExpended) + addBottomSheetCallback( + object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + val expanded = newState == BottomSheetBehavior.STATE_EXPANDED + if (expanded != isExpended) { + isExpended = expanded + callback(expanded) + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + }, + ) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt index 1dcead8e2..d17233c25 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Bundle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt @@ -1,6 +1,6 @@ @file:Suppress("DEPRECATION") -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.Intent import android.os.Build diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt index 8837856bc..d870f8d3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoilExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.Context import android.widget.ImageView @@ -12,9 +12,9 @@ import coil.request.SuccessResult import coil.util.CoilUtils import com.google.android.material.progressindicator.BaseProgressIndicator import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder +import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.image.RegionBitmapDecoder -import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? { val current = CoilUtils.result(this) @@ -23,6 +23,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image return null } } + disposeImageRequest() return ImageRequest.Builder(context) .data(data) .lifecycle(lifecycleOwner) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt index 879704445..9cb967878 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt @@ -1,9 +1,10 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import androidx.collection.ArrayMap import androidx.collection.ArraySet import java.util.Collections +@Deprecated("TODO: remove") fun MutableList.move(sourceIndex: Int, targetIndex: Int) { if (sourceIndex <= targetIndex) { Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt index 3120f2f68..35e2a1e56 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope @@ -8,9 +8,11 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.lifecycle.RetainedLifecycle import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine +import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -18,6 +20,9 @@ import kotlin.coroutines.resumeWithException val processLifecycleScope: LifecycleCoroutineScope inline get() = ProcessLifecycleOwner.get().lifecycleScope +val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope + inline get() = RetainedLifecycleCoroutineScope(this) + suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) { if (currentState.isAtLeast(state)) { return diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt index eeab153b0..3cec3da3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.ContentValues import android.database.Cursor @@ -36,4 +36,4 @@ fun JSONObject.toContentValues(): ContentValues { return cv } -private fun String.escapeName() = "`$this`" \ No newline at end of file +private fun String.escapeName() = "`$this`" diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Date.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Date.kt index 0a78f0341..e75842410 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DateExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Date.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.annotation.SuppressLint import android.text.format.DateUtils @@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this) fun Date.formatRelative(minResolution: Long): CharSequence = DateUtils.getRelativeTimeSpanString( - time, System.currentTimeMillis(), minResolution + time, System.currentTimeMillis(), minResolution, ) fun Date.daysDiff(other: Long): Int { @@ -27,4 +27,4 @@ fun Date.startOfDay(): Long { calendar[Calendar.SECOND] = 0 calendar[Calendar.MILLISECOND] = 0 return calendar.timeInMillis -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DisplayExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Display.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/DisplayExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Display.kt index 6f917ac1e..b8ca902d4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/DisplayExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Display.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.app.Activity import android.graphics.Rect diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/EventFlow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/EventFlow.kt new file mode 100644 index 000000000..11fc25beb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/EventFlow.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.core.util.ext + +import androidx.annotation.AnyThread +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.koitharu.kotatsu.core.util.Event + +@Suppress("FunctionName") +fun MutableEventFlow() = MutableStateFlow?>(null) + +typealias EventFlow = StateFlow?> + +typealias MutableEventFlow = MutableStateFlow?> + +@AnyThread +fun MutableEventFlow.call(data: T) { + value = Event(data) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index f2800f68c..79877887b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.ContentResolver import android.content.Context @@ -26,6 +26,8 @@ fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() } fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() } +fun File.isNotEmpty() = length() != 0L + fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use { it.readText() } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index 0ed270991..2aa0c1e62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.os.SystemClock import kotlinx.coroutines.delay @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transformLatest +import org.koitharu.kotatsu.R fun Flow.onFirst(action: suspend (T) -> Unit): Flow { var isFirstCall = true @@ -52,3 +53,12 @@ fun Flow>.flatten(): Flow = flow { } } } + +fun Flow.zipWithPrevious(): Flow> = flow { + var previous: T? = null + collect { value -> + val result = previous to value + previous = value + emit(result) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt new file mode 100644 index 000000000..d6c634468 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.core.util.ext + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.core.util.Event + +fun Flow.observe(owner: LifecycleOwner, collector: FlowCollector) { + val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT + owner.lifecycleScope.launch(start = start) { + collect(collector) + } +} + +fun Flow?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector) { + owner.lifecycleScope.launch { + owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { + it?.consume(collector) + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Fragment.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Fragment.kt index 1b696422e..d755911aa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Fragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.os.Bundle import androidx.annotation.MainThread diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/GraphicsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/GraphicsExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt index 94dc692a3..2e59b582f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/GraphicsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.graphics.Rect import kotlin.math.roundToInt @@ -10,4 +10,4 @@ fun Rect.scale(factor: Double) { (width() - newWidth) / 2, (height() - newHeight) / 2, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt similarity index 60% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt index a38596cb0..45463f045 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Http.kt @@ -1,5 +1,6 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext +import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response @@ -23,3 +24,17 @@ fun Response.parseJsonOrNull(): JSONObject? { closeQuietly() } } + +val HttpUrl.isHttpOrHttps: Boolean + get() { + val s = scheme.lowercase() + return s == "https" || s == "http" + } + +fun Response.ensureSuccess() = apply { + if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) { + val message = "Invalid response: $code $message at ${request.url}" + closeQuietly() + throw IllegalStateException(message) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt index 82a3780ce..d41e0ba38 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -8,8 +8,8 @@ import kotlinx.coroutines.withContext import okhttp3.ResponseBody import okio.BufferedSink import okio.Source -import org.koitharu.kotatsu.utils.CancellableSource -import org.koitharu.kotatsu.utils.progress.ProgressResponseBody +import org.koitharu.kotatsu.core.util.CancellableSource +import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody fun ResponseBody.withProgress(progressState: MutableStateFlow): ResponseBody { return ProgressResponseBody(this, progressState) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt index af2ddca22..eef3a3b45 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.view.View import androidx.core.graphics.Insets diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleListExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/LocaleList.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleListExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/LocaleList.kt index b6ae2535c..1c093a2d8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleListExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/LocaleList.kt @@ -1,7 +1,7 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import androidx.core.os.LocaleListCompat -import java.util.* +import java.util.Locale operator fun LocaleListCompat.iterator(): ListIterator = LocaleListCompatIterator(this) @@ -32,4 +32,4 @@ private class LocaleListCompatIterator(private val list: LocaleListCompat) : Lis override fun previous() = list.get(--index) ?: throw NoSuchElementException() override fun previousIndex() = index - 1 -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Network.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Network.kt index 07bdd0304..9f73ecfbf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/Network.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Network.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.Context import android.net.ConnectivityManager diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt new file mode 100644 index 000000000..046bd94ca --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.core.util.ext + +import org.koitharu.kotatsu.core.util.CompositeRunnable + +@Suppress("UNCHECKED_CAST") +fun Class.castOrNull(obj: Any?): T? { + if (obj == null || !isInstance(obj)) { + return null + } + return obj as T +} + +/* CompositeRunnable */ + +operator fun Runnable.plus(other: Runnable): Runnable { + val list = ArrayList(this.size + other.size) + if (this is CompositeRunnable) list.addAll(this) else list.add(this) + if (other is CompositeRunnable) list.addAll(other) else list.add(other) + return CompositeRunnable(list) +} + +private val Runnable.size: Int + get() = if (this is CompositeRunnable) size else 1 diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PreferencesExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/PreferencesExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt index d20c1eca6..72b7fc3bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PreferencesExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.SharedPreferences import androidx.preference.ListPreference @@ -22,4 +22,4 @@ fun > SharedPreferences.getEnumValue(key: String, defaultValue: E): fun > SharedPreferences.Editor.putEnumValue(key: String, value: E?) { putString(key, value?.name) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Primitive.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Primitive.kt index dee4a06d7..85fe52e38 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Primitive.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else this diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt new file mode 100644 index 000000000..0dd4d0cf2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.core.util.ext + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewHolder + +fun RecyclerView.clearItemDecorations() { + suppressLayout(true) + while (itemDecorationCount > 0) { + removeItemDecorationAt(0) + } + suppressLayout(false) +} + +fun RecyclerView.removeItemDecoration(cls: Class) { + repeat(itemDecorationCount) { i -> + if (cls.isInstance(getItemDecorationAt(i))) { + removeItemDecorationAt(i) + return + } + } +} + +var RecyclerView.firstVisibleItemPosition: Int + get() = (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() + ?: RecyclerView.NO_POSITION + set(value) { + if (value != RecyclerView.NO_POSITION) { + (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(value, 0) + } + } + +val RecyclerView.visibleItemCount: Int + get() = (layoutManager as? LinearLayoutManager)?.run { + findLastVisibleItemPosition() - findFirstVisibleItemPosition() + } ?: 0 + +fun RecyclerView.findCenterViewPosition(): Int { + val centerX = width / 2f + val centerY = height / 2f + val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION + return getChildAdapterPosition(view) +} + +fun RecyclerView.ViewHolder.getItem(clazz: Class): T? { + val rawItem = when (this) { + is AdapterDelegateViewBindingViewHolder<*, *> -> item + is AdapterDelegateViewHolder<*> -> item + else -> null + } ?: return null + return if (clazz.isAssignableFrom(rawItem.javaClass)) { + clazz.cast(rawItem) + } else { + null + } +} + +val RecyclerView.isScrolledToTop: Boolean + get() { + if (childCount == 0) { + return true + } + val holder = findViewHolderForAdapterPosition(0) + return holder != null && holder.itemView.top >= 0 + } + +val RecyclerView.LayoutManager?.firstVisibleItemPosition + get() = when (this) { + is LinearLayoutManager -> findFirstVisibleItemPosition() + is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0] + else -> 0 + } + +val RecyclerView.LayoutManager?.isLayoutReversed + get() = when (this) { + is LinearLayoutManager -> reverseLayout + is StaggeredGridLayoutManager -> reverseLayout + else -> false + } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ResourcesExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Resources.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/ResourcesExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Resources.kt index 187642b5b..6e75afee3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ResourcesExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Resources.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.annotation.SuppressLint import android.content.Context diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt index 5e4056a5d..b0124dd74 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt @@ -1,10 +1,10 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import androidx.annotation.FloatRange import org.koitharu.kotatsu.parsers.util.levenshteinDistance import java.util.UUID -inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String { +inline fun C?.ifNullOrEmpty(defaultValue: () -> C): C { return if (this.isNullOrEmpty()) defaultValue() else this } @@ -34,3 +34,9 @@ fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float) val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f) return diff < threshold } + +fun CharSequence.sanitize(): CharSequence { + return filterNot { c -> c.isReplacement() } +} + +fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF' diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/TextView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/TextView.kt index 305b1e5df..424e1e77c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/TextViewExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/TextView.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.graphics.Typeface import android.graphics.drawable.Drawable diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt index 7896da2e5..dae151af2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThemeExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt @@ -1,10 +1,11 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.Context import android.graphics.Color import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import androidx.annotation.Px import androidx.core.content.res.use import androidx.core.graphics.ColorUtils @@ -22,6 +23,22 @@ fun Context.getThemeColor( it.getColor(0, fallback) } +@Px +fun Context.getThemeDimensionPixelSize( + @AttrRes resId: Int, + @ColorInt fallback: Int = 0, +) = obtainStyledAttributes(intArrayOf(resId)).use { + it.getDimensionPixelSize(0, fallback) +} + +@Px +fun Context.getThemeDimensionPixelOffset( + @AttrRes resId: Int, + @ColorInt fallback: Int = 0, +) = obtainStyledAttributes(intArrayOf(resId)).use { + it.getDimensionPixelOffset(0, fallback) +} + @ColorInt fun Context.getThemeColor( @AttrRes resId: Int, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index c205ca939..29defe131 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -1,10 +1,9 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.ActivityNotFoundException import android.content.res.Resources import android.util.AndroidRuntimeException import androidx.collection.arraySetOf -import kotlinx.coroutines.CancellationException import okio.FileNotFoundException import okio.IOException import org.acra.ktx.sendWithAcra @@ -84,19 +83,10 @@ private val reportableExceptions = arraySetOf>( UnsupportedOperationException::class.java, ) -inline fun runCatchingCancellable(block: () -> R): Result { - return try { - Result.success(block()) - } catch (e: InterruptedException) { - throw e - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - Result.failure(e) - } -} - fun Throwable.isWebViewUnavailable(): Boolean { return (this is AndroidRuntimeException && message?.contains("WebView") == true) || cause?.isWebViewUnavailable() == true } + +@Suppress("FunctionName") +fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt index 599aff2e7..70816e0b1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.app.Activity import android.graphics.Rect @@ -9,13 +9,9 @@ import android.view.ViewParent import android.view.inputmethod.InputMethodManager import android.widget.Checkable import androidx.core.view.children -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ItemDecoration import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.slider.Slider -import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder -import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewHolder import kotlin.math.roundToInt fun View.hideKeyboard() { @@ -28,37 +24,6 @@ fun View.showKeyboard() { imm.showSoftInput(this, 0) } -fun RecyclerView.clearItemDecorations() { - suppressLayout(true) - while (itemDecorationCount > 0) { - removeItemDecorationAt(0) - } - suppressLayout(false) -} - -fun RecyclerView.removeItemDecoration(cls: Class) { - repeat(itemDecorationCount) { i -> - if (cls.isInstance(getItemDecorationAt(i))) { - removeItemDecorationAt(i) - return - } - } -} - -var RecyclerView.firstVisibleItemPosition: Int - get() = (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() - ?: RecyclerView.NO_POSITION - set(value) { - if (value != RecyclerView.NO_POSITION) { - (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(value, 0) - } - } - -val RecyclerView.visibleItemCount: Int - get() = (layoutManager as? LinearLayoutManager)?.run { - findLastVisibleItemPosition() - findFirstVisibleItemPosition() - } ?: 0 - fun View.hasGlobalPoint(x: Int, y: Int): Boolean { if (visibility != View.VISIBLE) { return false @@ -111,26 +76,6 @@ fun View.resetTransformations() { rotationY = 0f } -fun RecyclerView.findCenterViewPosition(): Int { - val centerX = width / 2f - val centerY = height / 2f - val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION - return getChildAdapterPosition(view) -} - -fun RecyclerView.ViewHolder.getItem(clazz: Class): T? { - val rawItem = when (this) { - is AdapterDelegateViewBindingViewHolder<*, *> -> item - is AdapterDelegateViewHolder<*> -> item - else -> null - } ?: return null - return if (clazz.isAssignableFrom(rawItem.javaClass)) { - clazz.cast(rawItem) - } else { - null - } -} - fun Slider.setValueRounded(newValue: Float) { val step = stepSize val roundedValue = if (step <= 0f) { @@ -141,15 +86,6 @@ fun Slider.setValueRounded(newValue: Float) { value = roundedValue.coerceIn(valueFrom, valueTo) } -val RecyclerView.isScrolledToTop: Boolean - get() { - if (childCount == 0) { - return true - } - val holder = findViewHolderForAdapterPosition(0) - return holder != null && holder.itemView.top >= 0 - } - fun ViewGroup.findViewsByType(clazz: Class): Sequence { if (childCount == 0) { return emptySequence() diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ViewModel.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ViewModel.kt index 3b840ade1..8fe5f8f47 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ViewModel.kt @@ -1,10 +1,12 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext +import android.annotation.SuppressLint import androidx.annotation.MainThread import androidx.fragment.app.Fragment import androidx.fragment.app.createViewModelLazy import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore import androidx.lifecycle.viewmodel.CreationExtras @MainThread @@ -17,3 +19,7 @@ inline fun Fragment.parentFragmentViewModels( extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras }, factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory }, ) + +val ViewModelStore.values: Collection + @SuppressLint("RestrictedApi") + get() = this.keys().mapNotNull { get(it) } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ImageRequestIndicatorListener.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ImageRequestIndicatorListener.kt index 1f1dc1b40..5b2d5bee8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ImageRequestIndicatorListener.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import coil.request.ErrorResult import coil.request.ImageRequest diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/IntPercentLabelFormatter.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/IntPercentLabelFormatter.kt index d9f4ff533..e9882986d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/IntPercentLabelFormatter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/IntPercentLabelFormatter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import android.content.Context import com.google.android.material.slider.LabelFormatter diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/PausingProgressJob.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/PausingProgressJob.kt index eba6501d9..7641646f8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/PausingProgressJob.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/PausingProgressJob.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import androidx.annotation.AnyThread import kotlinx.coroutines.Job diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressDeferred.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressDeferred.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressDeferred.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressDeferred.kt index 7fd1a9357..c1bad74c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressDeferred.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressDeferred.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow @@ -13,4 +13,4 @@ class ProgressDeferred( get() = progress.value fun progressAsFlow(): Flow

= progress -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt index 919d952ab..826916ddf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressJob.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressJob.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -13,4 +13,4 @@ open class ProgressJob

( get() = progress.value fun progressAsFlow(): Flow

= progress -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt index 20327a272..b66e5cd2a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ProgressResponseBody.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import kotlinx.coroutines.flow.MutableStateFlow import okhttp3.MediaType diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt index 97b83f52d..e83507ef1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/TimeLeftEstimator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/TimeLeftEstimator.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import android.os.SystemClock import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt new file mode 100644 index 000000000..350c66772 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsInteractor.kt @@ -0,0 +1,80 @@ +package org.koitharu.kotatsu.details.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.details.domain.model.DoubleManga +import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.model.LocalManga +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import javax.inject.Inject + +@Deprecated("") +class DetailsInteractor @Inject constructor( + private val historyRepository: HistoryRepository, + private val favouritesRepository: FavouritesRepository, + private val localMangaRepository: LocalMangaRepository, + private val trackingRepository: TrackingRepository, + private val settings: AppSettings, + private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, +) { + + fun observeIsFavourite(mangaId: Long): Flow { + return favouritesRepository.observeCategoriesIds(mangaId) + .map { it.isNotEmpty() } + } + + fun observeNewChapters(mangaId: Long): Flow { + return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled } + .flatMapLatest { isEnabled -> + if (isEnabled) { + trackingRepository.observeNewChaptersCount(mangaId) + } else { + flowOf(0) + } + } + } + + fun observeScrobblingInfo(mangaId: Long): Flow> { + return combine( + scrobblers.map { it.observeScrobblingInfo(mangaId) }, + ) { scrobblingInfo -> + scrobblingInfo.filterNotNull() + } + } + + fun observeIncognitoMode(mangaFlow: Flow): Flow { + return mangaFlow + .distinctUntilChangedBy { it?.isNsfw } + .flatMapLatest { manga -> + if (manga != null) { + historyRepository.observeShouldSkip(manga) + } else { + settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled } + } + } + } + + suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? { + return if (subject?.any?.id == localManga.manga.id) { + subject.copy( + localManga = runCatchingCancellable { + localMangaRepository.getDetails(localManga.manga) + }, + ) + } else { + subject + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt new file mode 100644 index 000000000..143d1ae24 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DoubleMangaLoadUseCase.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.details.domain + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.details.domain.model.DoubleManga +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.parsers.exception.NotFoundException +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import javax.inject.Inject + +class DoubleMangaLoadUseCase @Inject constructor( + private val mangaDataRepository: MangaDataRepository, + private val localMangaRepository: LocalMangaRepository, + private val mangaRepositoryFactory: MangaRepository.Factory, +) { + + suspend operator fun invoke(manga: Manga): DoubleManga = coroutineScope { + val remoteDeferred = async(Dispatchers.Default) { loadRemote(manga) } + val localDeferred = async(Dispatchers.Default) { loadLocal(manga) } + DoubleManga( + remoteManga = remoteDeferred.await(), + localManga = localDeferred.await(), + ) + } + + suspend operator fun invoke(mangaId: Long): DoubleManga { + val manga = mangaDataRepository.findMangaById(mangaId) ?: throwNFE() + return invoke(manga) + } + + suspend operator fun invoke(intent: MangaIntent): DoubleManga { + val manga = mangaDataRepository.resolveIntent(intent) ?: throwNFE() + return invoke(manga) + } + + private suspend fun loadLocal(manga: Manga): Result? { + return runCatchingCancellable { + if (manga.isLocal) { + localMangaRepository.getDetails(manga) + } else { + localMangaRepository.findSavedManga(manga)?.manga + } ?: return null + } + } + + private suspend fun loadRemote(manga: Manga): Result? { + return runCatchingCancellable { + val seed = if (manga.isLocal) { + localMangaRepository.getRemoteManga(manga) + } else { + manga + } ?: return null + val repository = mangaRepositoryFactory.create(seed.source) + repository.getDetails(seed) + } + } + + private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "") +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt new file mode 100644 index 000000000..732f59902 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/model/DoubleManga.kt @@ -0,0 +1,76 @@ +package org.koitharu.kotatsu.details.domain.model + +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.reader.data.filterChapters + +data class DoubleManga( + private val remoteManga: Result?, + private val localManga: Result?, +) { + + constructor(manga: Manga) : this( + remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null, + localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null, + ) + + val remote: Manga? + get() = remoteManga?.getOrNull() + + val local: Manga? + get() = localManga?.getOrNull() + + val any: Manga? + get() = remote ?: local + + val hasRemote: Boolean + get() = remoteManga?.isSuccess == true + + val hasLocal: Boolean + get() = localManga?.isSuccess == true + + val chapters: List? by lazy(LazyThreadSafetyMode.PUBLICATION) { + mergeChapters() + } + + fun requireAny(): Manga { + val result = remoteManga?.getOrNull() ?: localManga?.getOrNull() + if (result != null) { + return result + } + throw ( + remoteManga?.exceptionOrNull() + ?: localManga?.exceptionOrNull() + ?: IllegalStateException("No online either local manga available") + ) + } + + fun filterChapters(branch: String?) = DoubleManga( + remoteManga?.map { it.filterChapters(branch) }, + localManga?.map { it.filterChapters(branch) }, + ) + + private fun mergeChapters(): List? { + val remoteChapters = remote?.chapters + val localChapters = local?.chapters + if (localChapters == null && remoteChapters == null) { + return null + } + val localMap = if (!localChapters.isNullOrEmpty()) { + localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id } + } else { + null + } + val result = ArrayList(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0)) + remoteChapters?.forEach { r -> + localMap?.remove(r.id)?.let { l -> + result.add(l) + } ?: result.add(r) + } + localMap?.values?.let { + result.addAll(it) + } + return result + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt index 993894bfd..6c483d475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -4,18 +4,18 @@ import android.content.Context import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors -import org.koitharu.kotatsu.base.ui.CoroutineIntentService import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.core.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject @AndroidEntryPoint @@ -116,7 +116,7 @@ class MangaPrefetchService : CoroutineIntentService() { return false } val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java) - return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled() + return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/service/PrefetchCompanionEntryPoint.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt new file mode 100644 index 000000000..cc6a94502 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt @@ -0,0 +1,92 @@ +package org.koitharu.kotatsu.details.ui + +import android.transition.TransitionManager +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.graphics.Insets +import androidx.core.view.setMargins +import androidx.core.view.updateLayoutParams +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate +import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.databinding.ItemTipBinding +import com.google.android.material.R as materialR + +class ButtonTip( + private val root: ViewGroup, + private val insetsDelegate: WindowInsetsDelegate, + private val viewModel: DetailsViewModel, +) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener { + + private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false) + private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize) + + init { + selfBinding.textView.setText(R.string.details_button_tip) + selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap) + selfBinding.root.id = R.id.layout_tip + selfBinding.buttonClose.setOnClickListener(this) + } + + override fun onClick(v: View?) { + remove() + } + + override fun onWindowInsetsChanged(insets: Insets) { + if (root is CoordinatorLayout) { + selfBinding.root.updateLayoutParams { + bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize + } + } + } + + fun addToRoot() { + val lp: ViewGroup.LayoutParams = when (root) { + is CoordinatorLayout -> CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + // anchorId = R.id.layout_bottom + // anchorGravity = Gravity.TOP + gravity = Gravity.BOTTOM + setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) + bottomMargin += actionBarSize + } + + is ConstraintLayout -> ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width) + setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) + } + + else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + root.addView(selfBinding.root, lp) + if (root is ConstraintLayout) { + val cs = ConstraintSet() + cs.clone(root) + cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM) + cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START) + cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END) + cs.applyTo(root) + } + insetsDelegate.addInsetsListener(this) + } + + fun remove() { + if (root.context.isAnimationsEnabled) { + TransitionManager.beginDelayedTransition(root) + } + insetsDelegate.removeInsetsListener(this) + root.removeView(selfBinding.root) + viewModel.onButtonTipClosed() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt similarity index 74% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt index d40cb7195..727ff31f3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt @@ -5,19 +5,26 @@ import android.view.View.OnLayoutChangeListener import androidx.activity.OnBackPressedCallback import androidx.appcompat.view.ActionMode import com.google.android.material.bottomsheet.BottomSheetBehavior -import org.koitharu.kotatsu.base.ui.util.ActionModeListener -import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar +import org.koitharu.kotatsu.core.ui.util.ActionModeListener +import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged class ChaptersBottomSheetMediator( - bottomSheet: View, + private val behavior: BottomSheetBehavior<*>, ) : OnBackPressedCallback(false), ActionModeListener, - BottomSheetHeaderBar.OnExpansionChangeListener, OnLayoutChangeListener { - private val behavior = BottomSheetBehavior.from(bottomSheet) private var lockCounter = 0 + init { + behavior.doOnExpansionsChanged { isExpanded -> + isEnabled = isExpanded + if (!isExpanded) { + unlock() + } + } + } + override fun handleOnBackPressed() { behavior.state = BottomSheetBehavior.STATE_COLLAPSED } @@ -30,10 +37,6 @@ class ChaptersBottomSheetMediator( unlock() } - override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { - isEnabled = isExpanded - } - override fun onLayoutChange( v: View?, left: Int, @@ -58,6 +61,9 @@ class ChaptersBottomSheetMediator( fun unlock() { lockCounter-- + if (lockCounter < 0) { + lockCounter = 0 + } behavior.isDraggable = lockCounter <= 0 } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index 4915d3221..d1b6aa21d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.details.ui +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -12,19 +13,20 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.ListSelectionController -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderState -import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import kotlin.math.roundToInt class ChaptersFragment : @@ -37,17 +39,17 @@ class ChaptersFragment : private var chaptersAdapter: ChaptersAdapter? = null private var selectionController: ListSelectionController? = null - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentChaptersBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentChaptersBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) chaptersAdapter = ChaptersAdapter(this) selectionController = ListSelectionController( activity = requireActivity(), - decoration = ChaptersSelectionDecoration(view.context), + decoration = ChaptersSelectionDecoration(binding.root.context), registryOwner = this, callback = this, ) @@ -55,6 +57,9 @@ class ChaptersFragment : checkNotNull(selectionController).attachToRecyclerView(this) setHasFixedSize(true) adapter = chaptersAdapter + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + scrollIndicators = if (resources.getBoolean(R.bool.is_tablet)) 0 else View.SCROLL_INDICATOR_TOP + } } viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged) @@ -73,17 +78,12 @@ class ChaptersFragment : if (selectionController?.onItemClick(item.chapter.id) == true) { return } - if (item.hasFlag(ChapterListItem.FLAG_MISSING)) { - (activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id) - return - } startActivity( - ReaderActivity.newIntent( - context = view.context, - manga = viewModel.manga.value ?: return, - state = ReaderState(item.chapter.id, 0, 0), - ), - scaleUpActivityOptionsOf(view).toBundle(), + IntentBuilder(view.context) + .manga(viewModel.manga.value ?: return) + .state(ReaderState(item.chapter.id, 0, 0)) + .build(), + scaleUpActivityOptionsOf(view), ) } @@ -108,7 +108,7 @@ class ChaptersFragment : else -> { LocalChaptersRemoveService.start(requireContext(), manga, ids) Snackbar.make( - binding.recyclerViewChapters, + requireViewBinding().recyclerViewChapters, R.string.chapters_will_removed_background, Snackbar.LENGTH_LONG, ).show() @@ -164,12 +164,14 @@ class ChaptersFragment : val selectedIds = selectionController?.peekCheckedIds() ?: return false val allItems = chaptersAdapter?.items.orEmpty() val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds } - menu.findItem(R.id.action_save).isVisible = items.none { (_, x) -> - x.chapter.source == MangaSource.LOCAL - } - menu.findItem(R.id.action_delete).isVisible = items.all { (_, x) -> - x.chapter.source == MangaSource.LOCAL + var canSave = true + var canDelete = true + items.forEach { (_, x) -> + val isLocal = x.isDownloaded || x.chapter.source == MangaSource.LOCAL + if (isLocal) canSave = false else canDelete = false } + menu.findItem(R.id.action_save).isVisible = canSave + menu.findItem(R.id.action_delete).isVisible = canDelete menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size menu.findItem(R.id.action_mark_current).isVisible = items.size == 1 mode.title = items.size.toString() @@ -185,7 +187,7 @@ class ChaptersFragment : } override fun onSelectionChanged(controller: ListSelectionController, count: Int) { - binding.recyclerViewChapters.invalidateItemDecorations() + requireViewBinding().recyclerViewChapters.invalidateItemDecorations() } override fun onWindowInsetsChanged(insets: Insets) = Unit @@ -193,10 +195,13 @@ class ChaptersFragment : private fun onChaptersChanged(list: List) { val adapter = chaptersAdapter ?: return if (adapter.itemCount == 0) { - val position = list.indexOfFirst { it.hasFlag(ChapterListItem.FLAG_CURRENT) } - 1 + val position = list.indexOfFirst { it.isCurrent } - 1 if (position > 0) { val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt() - adapter.setItems(list, RecyclerViewScrollCallback(binding.recyclerViewChapters, position, offset)) + adapter.setItems( + list, + RecyclerViewScrollCallback(requireViewBinding().recyclerViewChapters, position, offset), + ) } else { adapter.items = list } @@ -206,6 +211,6 @@ class ChaptersFragment : } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading + requireViewBinding().progressBar.isVisible = isLoading } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt new file mode 100644 index 000000000..257d4bef5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt @@ -0,0 +1,66 @@ +package org.koitharu.kotatsu.details.ui + +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.details.ui.model.ChapterListItem +import org.koitharu.kotatsu.details.ui.model.toListItem +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.mapToSet + +fun mapChapters( + remoteManga: Manga?, + localManga: Manga?, + history: MangaHistory?, + newCount: Int, + branch: String?, + bookmarks: List, +): List { + val remoteChapters = remoteManga?.getChapters(branch).orEmpty() + val localChapters = localManga?.getChapters(branch).orEmpty() + if (remoteChapters.isEmpty() && localChapters.isEmpty()) { + return emptyList() + } + val bookmarked = bookmarks.mapToSet { it.chapterId } + val currentId = history?.chapterId ?: 0L + val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount + val chaptersSize = maxOf(remoteChapters.size, localChapters.size) + val ids = buildSet(chaptersSize) { + remoteChapters.mapTo(this) { it.id } + localChapters.mapTo(this) { it.id } + } + val result = ArrayList(chaptersSize) + val localMap = if (localChapters.isNotEmpty()) { + localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id } + } else { + null + } + var isUnread = currentId !in ids + for (chapter in remoteChapters) { + val local = localMap?.remove(chapter.id) + if (chapter.id == currentId) { + isUnread = true + } + result += chapter.toListItem( + isCurrent = chapter.id == currentId, + isUnread = isUnread, + isNew = isUnread && result.size >= newFrom, + isDownloaded = local != null, + isBookmarked = chapter.id in bookmarked, + ) + } + if (!localMap.isNullOrEmpty()) { + for (chapter in localMap.values) { + if (chapter.id == currentId) { + isUnread = true + } + result += chapter.toListItem( + isCurrent = chapter.id == currentId, + isUnread = isUnread, + isNew = false, + isDownloaded = remoteManga != null, + isBookmarked = chapter.id in bookmarked, + ) + } + } + return result +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt index c6c3fc64b..e42fa2ca8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt @@ -3,14 +3,18 @@ package org.koitharu.kotatsu.details.ui import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.SearchView import androidx.core.view.MenuProvider import org.koitharu.kotatsu.R +import java.lang.ref.WeakReference class ChaptersMenuProvider( private val viewModel: DetailsViewModel, private val bottomSheetMediator: ChaptersBottomSheetMediator?, -) : MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { +) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { + + private var searchItemRef: WeakReference? = null override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_chapters, menu) @@ -20,6 +24,7 @@ class ChaptersMenuProvider( searchView.setOnQueryTextListener(this) searchView.setIconifiedByDefault(false) searchView.queryHint = searchMenuItem.title + searchItemRef = WeakReference(searchMenuItem) } override fun onPrepareMenu(menu: Menu) { @@ -32,15 +37,22 @@ class ChaptersMenuProvider( viewModel.setChaptersReversed(!menuItem.isChecked) true } + else -> false } + override fun handleOnBackPressed() { + searchItemRef?.get()?.collapseActionView() + } + override fun onMenuItemActionExpand(item: MenuItem): Boolean { bottomSheetMediator?.lock() + isEnabled = true return true } override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + isEnabled = false (item.actionView as? SearchView)?.setQuery("", false) viewModel.performChapterSearch(null) bottomSheetMediator?.unlock() diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt similarity index 54% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index d5ac27063..1de67398f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -4,12 +4,14 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.os.Bundle +import android.transition.AutoTransition import android.transition.Slide import android.transition.TransitionManager import android.view.Gravity import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import android.view.animation.AccelerateDecelerateInterpolator import android.widget.Toast import androidx.activity.viewModels @@ -17,21 +19,31 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.lifecycle.Observer -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.filterNotNull import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.dialog.RecyclerViewAlertDialog -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.os.ShortcutsUpdater +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ViewBadge +import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.measureHeight +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.ui.adapter.branchAD @@ -41,29 +53,25 @@ import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.reader.ui.ReaderActivity -import org.koitharu.kotatsu.reader.ui.ReaderState -import org.koitharu.kotatsu.utils.ViewBadge -import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat -import org.koitharu.kotatsu.utils.ext.textAndVisible +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder +import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet +import java.lang.ref.WeakReference import javax.inject.Inject +import com.google.android.material.R as materialR @AndroidEntryPoint class DetailsActivity : BaseActivity(), View.OnClickListener, - BottomSheetHeaderBar.OnExpansionChangeListener, NoModalBottomSheetOwner, View.OnLongClickListener, PopupMenu.OnMenuItemClickListener { - override val bsHeader: BottomSheetHeaderBar? - get() = binding.headerChapters - @Inject lateinit var shortcutsUpdater: ShortcutsUpdater private lateinit var viewBadge: ViewBadge + private var buttonTip: WeakReference? = null private val viewModel: DetailsViewModel by viewModels() private lateinit var chaptersMenuProvider: ChaptersMenuProvider @@ -75,29 +83,35 @@ class DetailsActivity : setDisplayHomeAsUpEnabled(true) setDisplayShowTitleEnabled(false) } - binding.buttonRead.setOnClickListener(this) - binding.buttonRead.setOnLongClickListener(this) - binding.buttonDropdown.setOnClickListener(this) - viewBadge = ViewBadge(binding.buttonRead, this) - - chaptersMenuProvider = if (binding.layoutBottom != null) { - val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom)) + viewBinding.buttonRead.setOnClickListener(this) + viewBinding.buttonRead.setOnLongClickListener(this) + viewBinding.buttonDropdown.setOnClickListener(this) + viewBadge = ViewBadge(viewBinding.buttonRead, this) + + if (viewBinding.layoutBottom != null) { + val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom)) + val bsMediator = ChaptersBottomSheetMediator(behavior) actionModeDelegate.addListener(bsMediator) - checkNotNull(binding.headerChapters).addOnExpansionChangeListener(bsMediator) - checkNotNull(binding.headerChapters).addOnLayoutChangeListener(bsMediator) + checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator) onBackPressedDispatcher.addCallback(bsMediator) - ChaptersMenuProvider(viewModel, bsMediator) + chaptersMenuProvider = ChaptersMenuProvider(viewModel, bsMediator) + behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged) + viewBinding.toolbarChapters?.setNavigationOnClickListener { + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } } else { - ChaptersMenuProvider(viewModel, null) + chaptersMenuProvider = ChaptersMenuProvider(viewModel, null) + addMenuProvider(chaptersMenuProvider) } + onBackPressedDispatcher.addCallback(chaptersMenuProvider) - viewModel.manga.observe(this, ::onMangaUpdated) + viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) - viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) - viewModel.onError.observe( + viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) + viewModel.onError.observeEvent( this, SnackbarErrorObserver( - host = binding.containerDetails, + host = viewBinding.containerDetails, fragment = null, resolver = exceptionResolver, onResolved = { isResolved -> @@ -107,35 +121,39 @@ class DetailsActivity : }, ), ) - viewModel.onShowToast.observe(this) { + viewModel.onShowToast.observeEvent(this) { makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show() } + viewModel.onShowTip.observeEvent(this) { showTip() } viewModel.historyInfo.observe(this, ::onHistoryChanged) - viewModel.selectedBranchName.observe(this) { - binding.headerChapters?.subtitle = it - binding.textViewSubtitle?.textAndVisible = it + viewModel.selectedBranch.observe(this) { + viewBinding.toolbarChapters?.subtitle = it + viewBinding.textViewSubtitle?.textAndVisible = it } viewModel.isChaptersReversed.observe(this) { - binding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu() + viewBinding.toolbarChapters?.invalidateMenu() ?: invalidateOptionsMenu() } viewModel.favouriteCategories.observe(this) { invalidateOptionsMenu() } viewModel.branches.observe(this) { - binding.buttonDropdown.isVisible = it.size > 1 + viewBinding.buttonDropdown.isVisible = it.size > 1 } viewModel.chapters.observe(this, PrefetchObserver(this)) - viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(binding.containerDetails)) + viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails)) addMenuProvider( DetailsMenuProvider( activity = this, viewModel = viewModel, - snackbarHost = binding.containerChapters, + snackbarHost = viewBinding.containerChapters, shortcutsUpdater = shortcutsUpdater, ), ) - binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider) + } + + override fun getBottomSheetCollapsedHeight(): Int { + return viewBinding.layoutBsHeader?.measureHeight() ?: 0 } override fun onClick(v: View) { @@ -147,6 +165,8 @@ class DetailsActivity : override fun onLongClick(v: View): Boolean = when (v.id) { R.id.button_read -> { + buttonTip?.get()?.remove() + buttonTip = null val menu = PopupMenu(v.context, v) menu.inflate(R.menu.popup_read) menu.setOnMenuItemClickListener(this) @@ -158,31 +178,54 @@ class DetailsActivity : else -> false } - override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { - R.id.action_incognito -> { - openReader(isIncognitoMode = true) - true - } + override fun onMenuItemClick(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_incognito -> { + openReader(isIncognitoMode = true) + true + } - else -> false + R.id.action_pages_thumbs -> { + val history = viewModel.historyInfo.value.history + PagesThumbnailsSheet.show( + fm = supportFragmentManager, + manga = viewModel.manga.value ?: return false, + chapterId = history?.chapterId + ?: viewModel.chapters.value.firstOrNull()?.chapter?.id + ?: return false, + currentPage = history?.page ?: 0, + ) + true + } + + else -> false + } } - override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { + private fun onChaptersSheetStateChanged(isExpanded: Boolean) { + val toolbar = viewBinding.toolbarChapters ?: return + if (isAnimationsEnabled) { + val transition = AutoTransition() + transition.duration = getAnimationDuration(R.integer.config_shorterAnimTime) + TransitionManager.beginDelayedTransition(toolbar, transition) + } if (isExpanded) { - headerBar.addMenuProvider(chaptersMenuProvider) + toolbar.addMenuProvider(chaptersMenuProvider) + toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material) } else { - headerBar.removeMenuProvider(chaptersMenuProvider) + toolbar.removeMenuProvider(chaptersMenuProvider) + toolbar.navigationIcon = null } - binding.buttonRead.isGone = isExpanded + viewBinding.buttonRead.isGone = isExpanded } private fun onMangaUpdated(manga: Manga) { title = manga.title val hasChapters = !manga.chapters.isNullOrEmpty() - binding.buttonRead.isEnabled = hasChapters + viewBinding.buttonRead.isEnabled = hasChapters invalidateOptionsMenu() showBottomSheet(manga.chapters != null) - binding.groupHeader?.isVisible = hasChapters + viewBinding.groupHeader?.isVisible = hasChapters } private fun onMangaRemoved(manga: Manga) { @@ -195,17 +238,23 @@ class DetailsActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) if (insets.bottom > 0) { - window.setNavigationBarTransparentCompat(this, binding.layoutBottom?.elevation ?: 0f, 0.9f) + window.setNavigationBarTransparentCompat(this, viewBinding.layoutBottom?.elevation ?: 0f, 0.9f) + } + viewBinding.cardChapters?.updateLayoutParams { + bottomMargin = insets.bottom + marginEnd + } + viewBinding.dragHandle?.updateLayoutParams { + bottomMargin = insets.top } } private fun onHistoryChanged(info: HistoryInfo) { - with(binding.buttonRead) { + with(viewBinding.buttonRead) { if (info.history != null) { setText(R.string._continue) setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play) @@ -220,41 +269,14 @@ class DetailsActivity : info.totalChapters == 0 -> getString(R.string.no_chapters) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) } - binding.headerChapters?.title = text - binding.textViewTitle?.text = text + viewBinding.toolbarChapters?.title = text + viewBinding.textViewTitle?.text = text } private fun onNewChaptersChanged(newChapters: Int) { viewBadge.counter = newChapters } - fun showChapterMissingDialog(chapterId: Long) { - val remoteManga = viewModel.getRemoteManga() - if (remoteManga == null) { - val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) - snackbar.show() - return - } - MaterialAlertDialogBuilder(this).apply { - setMessage(R.string.chapter_is_missing_text) - setTitle(R.string.chapter_is_missing) - setNegativeButton(android.R.string.cancel, null) - setPositiveButton(R.string.read) { _, _ -> - startActivity( - ReaderActivity.newIntent( - context = this@DetailsActivity, - manga = remoteManga, - state = ReaderState(chapterId, 0, 0), - ), - ) - } - setNeutralButton(R.string.download) { _, _ -> - viewModel.download(setOf(chapterId)) - } - setCancelable(true) - }.show() - } - private fun showBranchPopupMenu() { var dialog: DialogInterface? = null val listener = OnListItemClickListener { item, _ -> @@ -266,24 +288,24 @@ class DetailsActivity : .setCancelable(true) .setNegativeButton(android.R.string.cancel, null) .setTitle(R.string.translations) - .setItems(viewModel.branches.value.orEmpty()) + .setItems(viewModel.branches.value) .create() .also { it.show() } } private fun openReader(isIncognitoMode: Boolean) { val manga = viewModel.manga.value ?: return - val chapterId = viewModel.historyInfo.value?.history?.chapterId + val chapterId = viewModel.historyInfo.value.history?.chapterId if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { - showChapterMissingDialog(chapterId) + val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) + snackbar.show() } else { startActivity( - ReaderActivity.newIntent( - context = this, - manga = manga, - branch = viewModel.selectedBranchValue, - isIncognitoMode = isIncognitoMode, - ), + IntentBuilder(this) + .manga(manga) + .branch(viewModel.selectedBranchValue) + .incognito(isIncognitoMode) + .build(), ) if (isIncognitoMode) { Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show() @@ -291,46 +313,52 @@ class DetailsActivity : } } - private fun isTabletLayout() = binding.layoutBottom == null - private fun showBottomSheet(isVisible: Boolean) { - val view = binding.layoutBottom ?: return + val view = viewBinding.layoutBottom ?: return if (view.isVisible == isVisible) return val transition = Slide(Gravity.BOTTOM) transition.addTarget(view) transition.interpolator = AccelerateDecelerateInterpolator() - TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transition) + TransitionManager.beginDelayedTransition(viewBinding.root as ViewGroup, transition) view.isVisible = isVisible } private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar { - val sb = Snackbar.make(binding.containerDetails, text, duration) - if (binding.layoutBottom?.isVisible == true) { - sb.anchorView = binding.headerChapters + val sb = Snackbar.make(viewBinding.containerDetails, text, duration) + if (viewBinding.layoutBottom?.isVisible == true) { + sb.anchorView = viewBinding.toolbarChapters } return sb } private class PrefetchObserver( private val context: Context, - ) : Observer?> { + ) : FlowCollector?> { private var isCalled = false - override fun onChanged(value: List?) { + override suspend fun emit(value: List?) { if (value.isNullOrEmpty()) { return } if (!isCalled) { isCalled = true - val item = value.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: value.first() + val item = value.find { it.isCurrent } ?: value.first() MangaPrefetchService.prefetchPages(context, item.chapter) } } } + private fun showTip() { + val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel) + tip.addToRoot() + buttonTip = WeakReference(tip) + } + companion object { + const val TIP_BUTTON = "btn_read" + fun newIntent(context: Context, manga: Manga): Intent { return Intent(context, DetailsActivity::class.java) .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt similarity index 72% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index f8ed4aa69..8272c3b2e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -18,22 +18,33 @@ import coil.request.ImageRequest import coil.util.CoilUtils import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter import org.koitharu.kotatsu.core.model.countChaptersByBranch -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.crossfade +import org.koitharu.kotatsu.core.util.ext.drawableTop +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.image.ui.ImageActivity +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource @@ -43,16 +54,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.SearchActivity -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.crossfade -import org.koitharu.kotatsu.utils.ext.drawableTop -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty -import org.koitharu.kotatsu.utils.ext.measureHeight -import org.koitharu.kotatsu.utils.ext.resolveDp -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf -import org.koitharu.kotatsu.utils.ext.textAndVisible -import org.koitharu.kotatsu.utils.image.CoverSizeResolver import javax.inject.Inject @AndroidEntryPoint @@ -66,23 +67,24 @@ class DetailsFragment : lateinit var coil: ImageLoader @Inject - lateinit var tagHighlighter: MangaTagHighlighter + lateinit var tagHighlighter: ListExtraProvider private val viewModel by activityViewModels() - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentDetailsBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentDetailsBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.textViewAuthor.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this) binding.infoLayout.textViewSource.setOnClickListener(this) binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.chipsTags.onChipClickListener = this - viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated) + TitleScrollCoordinator(binding.textViewTitle).attach(binding.scrollView) + viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) @@ -94,8 +96,8 @@ class DetailsFragment : override fun onItemClick(item: Bookmark, view: View) { startActivity( - ReaderActivity.newIntent(view.context, item), - scaleUpActivityOptionsOf(view).toBundle(), + ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(), + scaleUpActivityOptionsOf(view), ) Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() } @@ -114,7 +116,7 @@ class DetailsFragment : } private fun onMangaUpdated(manga: Manga) { - with(binding) { + with(requireViewBinding()) { // Main loadCover(manga) textViewTitle.text = manga.title @@ -159,7 +161,7 @@ class DetailsFragment : } private fun onChaptersChanged(chapters: List?) { - val infoLayout = binding.infoLayout + val infoLayout = requireViewBinding().infoLayout if (chapters.isNullOrEmpty()) { infoLayout.textViewChapters.isVisible = false } else { @@ -171,14 +173,14 @@ class DetailsFragment : private fun onDescriptionChanged(description: CharSequence?) { if (description.isNullOrBlank()) { - binding.textViewDescription.setText(R.string.no_description) + requireViewBinding().textViewDescription.setText(R.string.no_description) } else { - binding.textViewDescription.text = description + requireViewBinding().textViewDescription.text = description } } private fun onLocalSizeChanged(size: Long) { - val textView = binding.infoLayout.textViewSize + val textView = requireViewBinding().infoLayout.textViewSize if (size == 0L) { textView.isVisible = false } else { @@ -188,41 +190,41 @@ class DetailsFragment : } private fun onHistoryChanged(history: HistoryInfo) { - binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true) + requireViewBinding().progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true) } private fun onLoadingStateChanged(isLoading: Boolean) { if (isLoading) { - binding.progressBar.show() + requireViewBinding().progressBar.show() } else { - binding.progressBar.hide() + requireViewBinding().progressBar.hide() } } private fun onBookmarksChanged(bookmarks: List) { - var adapter = binding.recyclerViewBookmarks.adapter as? BookmarksAdapter - binding.groupBookmarks.isGone = bookmarks.isEmpty() + var adapter = requireViewBinding().recyclerViewBookmarks.adapter as? BookmarksAdapter + requireViewBinding().groupBookmarks.isGone = bookmarks.isEmpty() if (adapter != null) { adapter.items = bookmarks } else { adapter = BookmarksAdapter(coil, viewLifecycleOwner, this) adapter.items = bookmarks - binding.recyclerViewBookmarks.adapter = adapter + requireViewBinding().recyclerViewBookmarks.adapter = adapter val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing) - binding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing)) + requireViewBinding().recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing)) } } private fun onScrobblingInfoChanged(scrobblings: List) { - var adapter = binding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter - binding.recyclerViewScrobbling.isGone = scrobblings.isEmpty() + var adapter = requireViewBinding().recyclerViewScrobbling.adapter as? ScrollingInfoAdapter + requireViewBinding().recyclerViewScrobbling.isGone = scrobblings.isEmpty() if (adapter != null) { adapter.items = scrobblings } else { adapter = ScrollingInfoAdapter(viewLifecycleOwner, coil, childFragmentManager) adapter.items = scrobblings - binding.recyclerViewScrobbling.adapter = adapter - binding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) + requireViewBinding().recyclerViewScrobbling.adapter = adapter + requireViewBinding().recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration()) } } @@ -255,7 +257,7 @@ class DetailsFragment : manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, manga.source, ), - scaleUpActivityOptionsOf(v).toBundle(), + scaleUpActivityOptionsOf(v), ) } } @@ -267,9 +269,9 @@ class DetailsFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + requireViewBinding().root.updatePadding( bottom = ( - (activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight() + (activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight() ?.plus(insets.bottom)?.plus(resources.resolveDp(16)) ) ?: insets.bottom, @@ -277,11 +279,12 @@ class DetailsFragment : } private fun bindTags(manga: Manga) { - binding.chipsTags.setChips( + requireViewBinding().chipsTags.setChips( manga.tags.map { tag -> ChipsView.ChipModel( title = tag.title, - tint = tagHighlighter.getTint(tag), + tint = tagHighlighter.getTagTint(tag), + icon = 0, data = tag, isCheckable = false, isChecked = false, @@ -292,13 +295,13 @@ class DetailsFragment : private fun loadCover(manga: Manga) { val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } - val lastResult = CoilUtils.result(binding.imageViewCover) + val lastResult = CoilUtils.result(requireViewBinding().imageViewCover) if (lastResult?.request?.data == imageUrl) { return } val request = ImageRequest.Builder(context ?: return) - .target(binding.imageViewCover) - .size(CoverSizeResolver(binding.imageViewCover)) + .target(requireViewBinding().imageViewCover) + .size(CoverSizeResolver(requireViewBinding().imageViewCover)) .data(imageUrl) .tag(manga.source) .crossfade(requireContext()) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt index 73ff6a880..4d333c345 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt @@ -16,14 +16,14 @@ import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.os.ShortcutsUpdater +import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.details.ui.model.MangaBranch -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapNotNullToSet -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet +import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity -import org.koitharu.kotatsu.utils.ShareHelper class DetailsMenuProvider( private val activity: FragmentActivity, @@ -63,7 +63,7 @@ class DetailsMenuProvider( R.id.action_favourite -> { viewModel.manga.value?.let { - FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it) + FavouriteCategoriesSheet.show(activity.supportFragmentManager, it) } } @@ -105,7 +105,7 @@ class DetailsMenuProvider( R.id.action_scrobbling -> { viewModel.manga.value?.let { - ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it, null) + ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt similarity index 50% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index ecb76143b..2a7f0ae5d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -7,9 +7,7 @@ import android.text.style.ForegroundColorSpan import androidx.core.net.toUri import androidx.core.text.getSpans import androidx.core.text.parseAsHtml -import androidx.lifecycle.LiveData -import androidx.lifecycle.asFlow -import androidx.lifecycle.asLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -17,185 +15,178 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.update import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.core.model.getPreferredBranch +import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.computeSize +import org.koitharu.kotatsu.core.util.ext.requireValue +import org.koitharu.kotatsu.core.util.ext.sanitize +import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.details.domain.BranchComparator +import org.koitharu.kotatsu.details.domain.DetailsInteractor +import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase +import org.koitharu.kotatsu.details.domain.model.DoubleManga import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.HistoryInfo import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.computeSize -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.toFileOrNull -import java.io.IOException import javax.inject.Inject @HiltViewModel class DetailsViewModel @Inject constructor( private val historyRepository: HistoryRepository, - favouritesRepository: FavouritesRepository, - private val localMangaRepository: LocalMangaRepository, - trackingRepository: TrackingRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val imageGetter: Html.ImageGetter, - private val delegate: MangaDetailsDelegate, @LocalStorageChanges private val localStorageChanges: SharedFlow, private val downloadScheduler: DownloadWorker.Scheduler, + private val interactor: DetailsInteractor, + savedStateHandle: SavedStateHandle, + private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, + private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, ) : BaseViewModel() { + private val intent = MangaIntent(savedStateHandle) + private val mangaId = intent.mangaId + private val doubleManga: MutableStateFlow = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) private var loadingJob: Job - val onShowToast = SingleLiveEvent() - val onDownloadStarted = SingleLiveEvent() + val onShowToast = MutableEventFlow() + val onShowTip = MutableEventFlow() + val onDownloadStarted = MutableEventFlow() - private val history = historyRepository.observeOne(delegate.mangaId) + val manga = doubleManga.map { it?.any } + .stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any) + + val history = historyRepository.observeOne(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() } + val favouriteCategories = interactor.observeIsFavourite(mangaId) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled } - .flatMapLatest { isEnabled -> - if (isEnabled) { - trackingRepository.observeNewChaptersCount(delegate.mangaId) - } else { - flowOf(0) - } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) + val newChaptersCount = interactor.observeNewChapters(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) private val chaptersQuery = MutableStateFlow("") + val selectedBranch = MutableStateFlow(null) - private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse } - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - - val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext) - val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext) - val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext) - val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext) + val isChaptersReversed = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, + key = AppSettings.KEY_REVERSE_CHAPTERS, + valueProducer = { chaptersReverse }, + ) - val historyInfo: LiveData = combine( - delegate.manga, - delegate.selectedBranch, + val historyInfo: StateFlow = combine( + manga, + selectedBranch, history, - historyRepository.observeShouldSkip(delegate.manga), + interactor.observeIncognitoMode(manga), ) { m, b, h, im -> HistoryInfo(m, b, h, im) - }.asFlowLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, - defaultValue = HistoryInfo(null, null, null, false), + }.stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.Eagerly, + initialValue = HistoryInfo(null, null, null, false), ) - val bookmarks = delegate.manga.flatMapLatest { + val bookmarks = manga.flatMapLatest { if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - - val localSize = combine( - delegate.manga, - delegate.relatedManga, - ) { m1, m2 -> - val url = when { - m1?.source == MangaSource.LOCAL -> m1.url - m2?.source == MangaSource.LOCAL -> m2.url - else -> null - } - if (url != null) { - val file = url.toUri().toFileOrNull() - file?.computeSize() ?: 0L - } else { - 0L - } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) + + val localSize = doubleManga + .map { + val local = it?.local + if (local != null) { + val file = local.url.toUri().toFileOrNull() + file?.computeSize() ?: 0L + } else { + 0L + } + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0) - val description = delegate.manga + val description = manga .distinctUntilChangedBy { it?.description.orEmpty() } .transformLatest { val description = it?.description if (description.isNullOrEmpty()) { emit(null) } else { - emit(description.parseAsHtml().filterSpans()) + emit(description.parseAsHtml().filterSpans().sanitize()) emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans()) } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null) - val onMangaRemoved = SingleLiveEvent() + val onMangaRemoved = MutableEventFlow() val isScrobblingAvailable: Boolean get() = scrobblers.any { it.isAvailable } - val scrobblingInfo: LiveData> = combine( - scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) }, - ) { scrobblingInfo -> - scrobblingInfo.filterNotNull() - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + val scrobblingInfo: StateFlow> = interactor.observeScrobblingInfo(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) - val branches: LiveData> = combine( - delegate.manga, - delegate.selectedBranch, + val branches: StateFlow> = combine( + doubleManga, + selectedBranch, ) { m, b -> - val chapters = m?.chapters ?: return@combine emptyList() + val chapters = m?.chapters + if (chapters.isNullOrEmpty()) return@combine emptyList() chapters.groupBy { x -> x.branch } .map { x -> MangaBranch(x.key, x.value.size, x.key == b) } .sortedWith(BranchComparator()) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) - val selectedBranchName = delegate.selectedBranch - .asFlowLiveData(viewModelScope.coroutineContext, null) - - val isChaptersEmpty: LiveData = combine( - delegate.manga, - isLoading.asFlow(), - ) { m, loading -> - m != null && m.chapters.isNullOrEmpty() && !loading - }.asFlowLiveData(viewModelScope.coroutineContext, false) + val isChaptersEmpty: StateFlow = combine( + doubleManga, + isLoading, + ) { manga, loading -> + manga?.any != null && manga.chapters.isNullOrEmpty() && !loading + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) val chapters = combine( combine( - delegate.manga, - delegate.relatedManga, + doubleManga, history, - delegate.selectedBranch, - newChapters, - ) { manga, related, history, branch, news -> - delegate.mapChapters(manga, related, history, news, branch) + selectedBranch, + newChaptersCount, + bookmarks, + ) { manga, history, branch, news, bookmarks -> + mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks) }, - chaptersReversed, + isChaptersReversed, chaptersQuery, ) { list, reversed, query -> (if (reversed) list.asReversed() else list).filterSearch(query) - }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) val selectedBranchValue: String? - get() = delegate.selectedBranch.value + get() = selectedBranch.value init { loadingJob = doLoad() @@ -203,6 +194,12 @@ class DetailsViewModel @Inject constructor( localStorageChanges .collect { onDownloadComplete(it) } } + launchJob(Dispatchers.Default) { + if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) { + manga.filterNot { it?.chapters.isNullOrEmpty() }.first() + onShowTip.call(Unit) + } + } } fun reload() { @@ -211,26 +208,20 @@ class DetailsViewModel @Inject constructor( } fun deleteLocal() { - val m = delegate.manga.value + val m = doubleManga.value?.local if (m == null) { onShowToast.call(R.string.file_not_found) return } launchLoadingJob(Dispatchers.Default) { - val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)?.manga - checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } - val original = localMangaRepository.getRemoteManga(manga) - localMangaRepository.delete(manga) || throw IOException("Unable to delete file") - runCatchingCancellable { - historyRepository.deleteOrSwap(manga, original) - } - onMangaRemoved.emitCall(manga) + deleteLocalMangaUseCase(m) + onMangaRemoved.call(m) } } fun removeBookmark(bookmark: Bookmark) { - launchJob { - bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId) + launchJob(Dispatchers.Default) { + bookmarksRepository.removeBookmark(bookmark) onShowToast.call(R.string.bookmark_removed) } } @@ -240,11 +231,7 @@ class DetailsViewModel @Inject constructor( } fun setSelectedBranch(branch: String?) { - delegate.selectedBranch.value = branch - } - - fun getRemoteManga(): Manga? { - return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } + selectedBranch.value = branch } fun performChapterSearch(query: String?) { @@ -255,7 +242,7 @@ class DetailsViewModel @Inject constructor( val scrobbler = getScrobbler(index) ?: return launchJob(Dispatchers.Default) { scrobbler.updateScrobblingInfo( - mangaId = delegate.mangaId, + mangaId = mangaId, rating = rating, status = status, comment = null, @@ -267,34 +254,49 @@ class DetailsViewModel @Inject constructor( val scrobbler = getScrobbler(index) ?: return launchJob(Dispatchers.Default) { scrobbler.unregisterScrobbling( - mangaId = delegate.mangaId, + mangaId = mangaId, ) } } fun markChapterAsCurrent(chapterId: Long) { launchJob(Dispatchers.Default) { - val manga = checkNotNull(delegate.manga.value) - val chapters = checkNotNull(manga.getChapters(selectedBranchValue)) + val manga = checkNotNull(doubleManga.value) + val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters) val chapterIndex = chapters.indexOfFirst { it.id == chapterId } check(chapterIndex in chapters.indices) { "Chapter not found" } val percent = chapterIndex / chapters.size.toFloat() - historyRepository.addOrUpdate(manga = manga, chapterId = chapterId, page = 0, scroll = 0, percent = percent) + historyRepository.addOrUpdate( + manga = manga.requireAny(), + chapterId = chapterId, + page = 0, + scroll = 0, + percent = percent, + ) } } fun download(chaptersIds: Set?) { launchJob(Dispatchers.Default) { downloadScheduler.schedule( - getRemoteManga() ?: checkNotNull(manga.value), + doubleManga.requireValue().requireAny(), chaptersIds, ) - onDownloadStarted.emitCall(Unit) + onDownloadStarted.call(Unit) } } + fun onButtonTipClosed() { + settings.closeTip(DetailsActivity.TIP_BUTTON) + } + private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - delegate.doLoad() + val result = doubleMangaLoadUseCase(intent) + val manga = result.requireAny() + // find default branch + val hist = historyRepository.getOne(manga) + selectedBranch.value = manga.getPreferredBranch(hist) + doubleManga.value = result } private fun List.filterSearch(query: String): List { @@ -308,21 +310,9 @@ class DetailsViewModel @Inject constructor( private suspend fun onDownloadComplete(downloadedManga: LocalManga?) { downloadedManga ?: return - val currentManga = delegate.manga.value ?: return - if (currentManga.id != downloadedManga.manga.id) { - return - } - if (currentManga.source == MangaSource.LOCAL) { - reload() - } else { - viewModelScope.launch(Dispatchers.Default) { - runCatchingCancellable { - localMangaRepository.getDetails(downloadedManga.manga) - }.onSuccess { - delegate.relatedManga.value = it - }.onFailure { - it.printStackTraceDebug() - } + launchJob { + doubleManga.update { + interactor.updateLocal(it, downloadedManga) } } } @@ -337,7 +327,7 @@ class DetailsViewModel @Inject constructor( } private fun getScrobbler(index: Int): Scrobbler? { - val info = scrobblingInfo.value?.getOrNull(index) + val info = scrobblingInfo.value.getOrNull(index) val scrobbler = if (info != null) { scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable } } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/MangaDetailsAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/MangaDetailsAdapter.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleScrollCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleScrollCoordinator.kt new file mode 100644 index 000000000..364357531 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleScrollCoordinator.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.details.ui + +import android.content.Context +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.doOnLayout +import androidx.core.widget.NestedScrollView +import org.koitharu.kotatsu.core.util.ext.findActivity +import java.lang.ref.WeakReference + +class TitleScrollCoordinator( + private val titleView: TextView, +) : NestedScrollView.OnScrollChangeListener { + + private val location = IntArray(2) + private var activityRef: WeakReference? = null + + override fun onScrollChange(v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) { + val actionBar = getActivity(v.context)?.supportActionBar ?: return + titleView.getLocationOnScreen(location) + var top = location[1] + titleView.height + v.getLocationOnScreen(location) + top -= location[1] + actionBar.setDisplayShowTitleEnabled(top < 0) + } + + fun attach(scrollView: NestedScrollView) { + scrollView.setOnScrollChangeListener(this) + scrollView.doOnLayout { + onScrollChange(scrollView, 0, 0, 0, 0) + } + } + + private fun getActivity(context: Context): AppCompatActivity? { + activityRef?.get()?.let { + if (!it.isDestroyed) return it + } + val activity = context.findActivity() as? AppCompatActivity + if (activity == null || activity.isDestroyed) { + return null + } + activityRef = WeakReference(activity) + return activity + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt index 5e11ad8d2..d471a5c0b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt @@ -7,11 +7,11 @@ import android.text.style.RelativeSizeSpan import androidx.core.text.buildSpannedString import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding import org.koitharu.kotatsu.details.ui.model.MangaBranch -import org.koitharu.kotatsu.utils.ext.getThemeColor fun branchAD( clickListener: OnListItemClickListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt index cc3f73f73..add2a583f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt @@ -1,7 +1,7 @@ package org.koitharu.kotatsu.details.ui.adapter import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.details.ui.model.MangaBranch class BranchesAdapter( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt new file mode 100644 index 000000000..9865b59cb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt @@ -0,0 +1,57 @@ +package org.koitharu.kotatsu.details.ui.adapter + +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.drawableStart +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.textAndVisible +import org.koitharu.kotatsu.databinding.ItemChapterBinding +import org.koitharu.kotatsu.details.ui.model.ChapterListItem +import com.google.android.material.R as materialR + +fun chapterListItemAD( + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }, +) { + + val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener) + itemView.setOnClickListener(eventListener) + itemView.setOnLongClickListener(eventListener) + + bind { payloads -> + if (payloads.isEmpty()) { + binding.textViewTitle.text = item.chapter.name + binding.textViewNumber.text = item.chapter.number.toString() + binding.textViewDescription.textAndVisible = item.description() + } + when { + item.isCurrent -> { + binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_primary) + binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnPrimary)) + } + + item.isUnread -> { + binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default) + binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnTertiary)) + } + + else -> { + binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline) + binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary)) + } + } + binding.imageViewBookmarked.isVisible = item.isBookmarked + binding.imageViewDownloaded.isVisible = item.isDownloaded + // binding.imageViewNew.isVisible = item.isNew + binding.textViewTitle.drawableStart = if (item.isNew) { + ContextCompat.getDrawable(context, R.drawable.ic_new) + } else { + null + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt index 7b91abef5..d1de826d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt @@ -3,8 +3,8 @@ package org.koitharu.kotatsu.details.ui.adapter import android.content.Context import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.details.ui.model.ChapterListItem import kotlin.jvm.internal.Intrinsics diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt index 469ae6514..505de1c4b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt @@ -8,9 +8,9 @@ import android.graphics.RectF import android.view.View import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.util.ext.getThemeColor import com.google.android.material.R as materialR -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration -import org.koitharu.kotatsu.utils.ext.getThemeColor class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt index 35b7aec12..5c3cfd46e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt @@ -22,12 +22,20 @@ class ChapterListItem( return field } - val status: Int - get() = flags and MASK_STATUS + val isCurrent: Boolean + get() = hasFlag(FLAG_CURRENT) - fun hasFlag(flag: Int): Boolean { - return (flags and flag) == flag - } + val isUnread: Boolean + get() = hasFlag(FLAG_UNREAD) + + val isDownloaded: Boolean + get() = hasFlag(FLAG_DOWNLOADED) + + val isBookmarked: Boolean + get() = hasFlag(FLAG_BOOKMARKED) + + val isNew: Boolean + get() = hasFlag(FLAG_NEW) fun description(): CharSequence? { val scanlator = chapter.scanlator?.takeUnless { it.isBlank() } @@ -38,6 +46,10 @@ class ChapterListItem( } } + private fun hasFlag(flag: Int): Boolean { + return (flags and flag) == flag + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -46,9 +58,7 @@ class ChapterListItem( if (chapter != other.chapter) return false if (flags != other.flags) return false - if (uploadDateMs != other.uploadDateMs) return false - - return true + return uploadDateMs == other.uploadDateMs } override fun hashCode(): Int { @@ -63,8 +73,7 @@ class ChapterListItem( const val FLAG_UNREAD = 2 const val FLAG_CURRENT = 4 const val FLAG_NEW = 8 - const val FLAG_MISSING = 16 + const val FLAG_BOOKMARKED = 16 const val FLAG_DOWNLOADED = 32 - const val MASK_STATUS = FLAG_UNREAD or FLAG_CURRENT } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt index 8f555e39c..95c4cae15 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt @@ -1,8 +1,8 @@ package org.koitharu.kotatsu.details.ui.model +import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_BOOKMARKED import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED -import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -11,14 +11,14 @@ fun MangaChapter.toListItem( isCurrent: Boolean, isUnread: Boolean, isNew: Boolean, - isMissing: Boolean, isDownloaded: Boolean, + isBookmarked: Boolean, ): ChapterListItem { var flags = 0 if (isCurrent) flags = flags or FLAG_CURRENT if (isUnread) flags = flags or FLAG_UNREAD if (isNew) flags = flags or FLAG_NEW - if (isMissing) flags = flags or FLAG_MISSING + if (isBookmarked) flags = flags or FLAG_BOOKMARKED if (isDownloaded) flags = flags or FLAG_DOWNLOADED return ChapterListItem( chapter = this, diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt index 93f32add9..42644501a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoAD.kt @@ -5,11 +5,11 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest fun scrobblingInfoAD( lifecycleOwner: LifecycleOwner, @@ -19,7 +19,7 @@ fun scrobblingInfoAD( { layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) }, ) { binding.root.setOnClickListener { - ScrobblingInfoBottomSheet.show(fragmentManager, bindingAdapterPosition) + ScrobblingInfoSheet.show(fragmentManager, bindingAdapterPosition) } bind { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt similarity index 61% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt index dfaae62fd..faa310f6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoSheet.kt @@ -17,23 +17,26 @@ import androidx.fragment.app.activityViewModels import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.sanitize +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetScrobblingBinding import org.koitharu.kotatsu.details.ui.DetailsViewModel import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus -import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf -import org.koitharu.kotatsu.utils.ext.withArgs +import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import javax.inject.Inject @AndroidEntryPoint -class ScrobblingInfoBottomSheet : - BaseBottomSheet(), +class ScrobblingInfoSheet : + BaseAdaptiveSheet(), AdapterView.OnItemSelectedListener, RatingBar.OnRatingBarChangeListener, View.OnClickListener, @@ -52,15 +55,16 @@ class ScrobblingInfoBottomSheet : scrobblerIndex = requireArguments().getInt(ARG_INDEX, scrobblerIndex) } - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding { return SheetScrobblingBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: SheetScrobblingBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged) - viewModel.onError.observe(viewLifecycleOwner) { - Toast.makeText(view.context, it.getDisplayMessage(view.resources), Toast.LENGTH_SHORT).show() + viewModel.onError.observeEvent(viewLifecycleOwner) { + Toast.makeText(binding.root.context, it.getDisplayMessage(binding.root.resources), Toast.LENGTH_SHORT) + .show() } binding.spinnerStatus.onItemSelectedListener = this @@ -69,9 +73,9 @@ class ScrobblingInfoBottomSheet : binding.imageViewCover.setOnClickListener(this) binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() - menu = PopupMenu(view.context, binding.buttonMenu).apply { + menu = PopupMenu(binding.root.context, binding.buttonMenu).apply { inflate(R.menu.opt_scrobbling) - setOnMenuItemClickListener(this@ScrobblingInfoBottomSheet) + setOnMenuItemClickListener(this@ScrobblingInfoSheet) } } @@ -83,7 +87,7 @@ class ScrobblingInfoBottomSheet : override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { viewModel.updateScrobbling( index = scrobblerIndex, - rating = binding.ratingBar.rating / binding.ratingBar.numStars, + rating = requireViewBinding().ratingBar.rating / requireViewBinding().ratingBar.numStars, status = enumValues().getOrNull(position), ) } @@ -95,7 +99,7 @@ class ScrobblingInfoBottomSheet : viewModel.updateScrobbling( index = scrobblerIndex, rating = rating / ratingBar.numStars, - status = enumValues().getOrNull(binding.spinnerStatus.selectedItemPosition), + status = enumValues().getOrNull(requireViewBinding().spinnerStatus.selectedItemPosition), ) } } @@ -104,9 +108,9 @@ class ScrobblingInfoBottomSheet : when (v.id) { R.id.button_menu -> menu?.show() R.id.imageView_cover -> { - val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return + val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return val options = scaleUpActivityOptionsOf(v) - startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle()) + startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options) } } } @@ -117,13 +121,13 @@ class ScrobblingInfoBottomSheet : dismissAllowingStateLoss() return } - binding.textViewTitle.text = scrobbling.title - binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars - binding.textViewDescription.text = scrobbling.description - binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1) - binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId) - binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId) - binding.imageViewCover.newImageRequest(viewLifecycleOwner, scrobbling.coverUrl)?.apply { + requireViewBinding().textViewTitle.text = scrobbling.title + requireViewBinding().ratingBar.rating = scrobbling.rating * requireViewBinding().ratingBar.numStars + requireViewBinding().textViewDescription.text = scrobbling.description?.sanitize() + requireViewBinding().spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1) + requireViewBinding().imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId) + requireViewBinding().imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId) + requireViewBinding().imageViewCover.newImageRequest(viewLifecycleOwner, scrobbling.coverUrl)?.apply { placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) error(R.drawable.ic_error_placeholder) @@ -134,7 +138,7 @@ class ScrobblingInfoBottomSheet : override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.action_browser -> { - val url = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.externalUrl ?: return false + val url = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.externalUrl ?: return false val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity( Intent.createChooser(intent, getString(R.string.open_in_browser)), @@ -148,8 +152,8 @@ class ScrobblingInfoBottomSheet : R.id.action_edit -> { val manga = viewModel.manga.value ?: return false - val scrobblerService = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.scrobbler - ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService) + val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler + ScrobblingSelectorSheet.show(parentFragmentManager, manga, scrobblerService) dismiss() } } @@ -161,7 +165,7 @@ class ScrobblingInfoBottomSheet : private const val TAG = "ScrobblingInfoBottomSheet" private const val ARG_INDEX = "index" - fun show(fm: FragmentManager, index: Int) = ScrobblingInfoBottomSheet().withArgs(1) { + fun show(fm: FragmentManager, index: Int) = ScrobblingInfoSheet().withArgs(1) { putInt(ARG_INDEX, index) }.show(fm, TAG) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingItemDecoration.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/details/ui/scrobbling/ScrollingInfoAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt index fea793f6d..8adf4a953 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt @@ -1,8 +1,8 @@ package org.koitharu.kotatsu.download.domain import androidx.work.Data -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.history.data.PROGRESS_NONE +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import java.util.Date diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt index 7517cb0d1..e2150152a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -7,14 +7,14 @@ import androidx.work.WorkInfo import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.util.format -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.ext.textAndVisible fun downloadItemAD( lifecycleOwner: LifecycleOwner, diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt index c4dd45699..d72a541c2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.download.ui.list -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener interface DownloadItemListener : OnListItemClickListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt index ddc1e9154..5a6128b75 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt @@ -11,14 +11,16 @@ import androidx.annotation.Px import androidx.appcompat.view.ActionMode import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import androidx.lifecycle.Observer import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.FlowCollector import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.list.ListSelectionController -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.PausingReceiver @@ -51,7 +53,7 @@ class DownloadsActivity : BaseActivity(), registryOwner = this, callback = this, ) - with(binding.recyclerView) { + with(viewBinding.recyclerView) { setHasFixedSize(true) addItemDecoration(decoration) adapter = downloadsAdapter @@ -61,20 +63,20 @@ class DownloadsActivity : BaseActivity(), viewModel.items.observe(this) { downloadsAdapter.items = it } - viewModel.onActionDone.observe(this, ReversibleActionObserver(binding.recyclerView)) - val menuObserver = Observer { _ -> invalidateOptionsMenu() } + viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView)) + val menuObserver = FlowCollector { _ -> invalidateOptionsMenu() } viewModel.hasActiveWorks.observe(this, menuObserver) viewModel.hasPausedWorks.observe(this, menuObserver) viewModel.hasCancellableWorks.observe(this, menuObserver) } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( left = insets.left + listSpacing, right = insets.right + listSpacing, bottom = insets.bottom, ) - binding.toolbar.updatePadding( + viewBinding.toolbar.updatePadding( left = insets.left, right = insets.right, ) @@ -104,7 +106,7 @@ class DownloadsActivity : BaseActivity(), } override fun onSelectionChanged(controller: ListSelectionController, count: Int) { - binding.recyclerView.invalidateItemDecorations() + viewBinding.recyclerView.invalidateItemDecorations() } override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt index 2778efdc4..16fa29387 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt index 89428502e..3429ee2c9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.settings.SettingsActivity class DownloadsMenuProvider( private val context: Context, @@ -23,6 +24,10 @@ class DownloadsMenuProvider( R.id.action_resume -> viewModel.resumeAll() R.id.action_cancel_all -> confirmCancelAll() R.id.action_remove_completed -> confirmRemoveCompleted() + R.id.action_settings -> { + context.startActivity(SettingsActivity.newDownloadsSettingsIntent(context)) + } + else -> return false } return true diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt index 0ca62de1b..eb47bc515 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt @@ -12,9 +12,9 @@ import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration -import org.koitharu.kotatsu.utils.ext.getItem -import org.koitharu.kotatsu.utils.ext.getThemeColor +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.util.ext.getItem +import org.koitharu.kotatsu.core.util.ext.getThemeColor import com.google.android.material.R as materialR class DownloadsSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt index cdcf5c4db..24b3dc68f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -16,10 +16,13 @@ import kotlinx.coroutines.plus import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.daysDiff import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -27,9 +30,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.daysDiff import java.util.Date import java.util.UUID import java.util.concurrent.TimeUnit @@ -47,23 +47,23 @@ class DownloadsViewModel @Inject constructor( .mapLatest { it.toDownloadsList() } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - val onActionDone = SingleLiveEvent() + val onActionDone = MutableEventFlow() val items = works.map { it?.toUiList() ?: listOf(LoadingState) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) val hasPausedWorks = works.map { it?.any { x -> x.canResume } == true - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) val hasActiveWorks = works.map { it?.any { x -> x.canPause } == true - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) val hasCancellableWorks = works.map { it?.any { x -> !x.workState.isFinished } == true - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) fun cancel(id: UUID) { launchJob(Dispatchers.Default) { @@ -79,14 +79,14 @@ class DownloadsViewModel @Inject constructor( workScheduler.cancel(work.id) } } - onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null)) + onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null)) } } fun cancelAll() { launchJob(Dispatchers.Default) { workScheduler.cancelAll() - onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null)) + onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null)) } } @@ -146,14 +146,14 @@ class DownloadsViewModel @Inject constructor( workScheduler.delete(work.id) } } - onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null)) + onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) } } fun removeCompleted() { launchJob(Dispatchers.Default) { workScheduler.removeCompleted() - onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null)) + onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 21f57cf3d..41568e832 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -23,6 +23,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.list.DownloadsActivity @@ -31,8 +32,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.utils.ext.getDrawableOrThrow -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.util.UUID import com.google.android.material.R as materialR diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt index 69453b711..6c42b9275 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt @@ -1,18 +1,18 @@ package org.koitharu.kotatsu.download.ui.worker import android.view.View -import androidx.lifecycle.Observer import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.flow.FlowCollector import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner -import org.koitharu.kotatsu.utils.ext.findActivity class DownloadStartedObserver( private val snackbarHost: View, -) : Observer { +) : FlowCollector { - override fun onChanged(value: Unit) { + override suspend fun emit(value: Unit) { val snackbar = Snackbar.make(snackbarHost, R.string.download_started, Snackbar.LENGTH_LONG) (snackbarHost.context.findActivity() as? BottomNavOwner)?.let { snackbar.anchorView = it.bottomNav diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 49c285fa1..d1c18925a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -35,31 +35,32 @@ import okio.IOException import okio.buffer import okio.sink import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.Throttler +import org.koitharu.kotatsu.core.util.WorkManagerHelper +import org.koitharu.kotatsu.core.util.ext.deleteAwait +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.writeAllCancellable +import org.koitharu.kotatsu.core.util.progress.TimeLeftEstimator import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.utils.Throttler -import org.koitharu.kotatsu.utils.WorkManagerHelper -import org.koitharu.kotatsu.utils.ext.deleteAwait -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.writeAllCancellable -import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import java.util.UUID import java.util.concurrent.TimeUnit @@ -69,7 +70,7 @@ import javax.inject.Inject class DownloadWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted params: WorkerParameters, - private val okHttp: OkHttpClient, + @MangaHttpClient private val okHttp: OkHttpClient, private val cache: PagesCache, private val localMangaRepository: LocalMangaRepository, private val mangaDataRepository: MangaDataRepository, diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt index 353911801..71dc7aa4c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt @@ -7,7 +7,7 @@ import android.content.IntentFilter import android.net.Uri import android.os.PatternMatcher import androidx.core.app.PendingIntentCompat -import org.koitharu.kotatsu.utils.ext.toUUIDOrNull +import org.koitharu.kotatsu.core.util.ext.toUUIDOrNull import java.util.UUID class PausingReceiver( diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt index 5fbce6f56..c91336c1c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt @@ -2,14 +2,14 @@ package org.koitharu.kotatsu.explore.domain import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.core.util.ext.almostEquals +import org.koitharu.kotatsu.core.util.ext.asArrayList +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist -import org.koitharu.kotatsu.utils.ext.almostEquals -import org.koitharu.kotatsu.utils.ext.asArrayList -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject class ExploreRepository @Inject constructor( diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index 618dc4763..bf58733d5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -16,14 +16,17 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.dialog.TwoButtonsAlertDialog -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner -import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver -import org.koitharu.kotatsu.base.ui.util.SpanSizeResolver import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter @@ -36,7 +39,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity -import org.koitharu.kotatsu.utils.ext.addMenuProvider import javax.inject.Inject @AndroidEntryPoint @@ -54,14 +56,14 @@ class ExploreFragment : private var paddingHorizontal = 0 override val recyclerView: RecyclerView - get() = binding.recyclerView + get() = requireViewBinding().recyclerView - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding { return FragmentExploreBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this) with(binding.recyclerView) { adapter = exploreAdapter @@ -70,15 +72,15 @@ class ExploreFragment : val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) paddingHorizontal = spacing } - addMenuProvider(ExploreMenuProvider(view.context, viewModel)) + addMenuProvider(ExploreMenuProvider(binding.root.context, viewModel)) viewModel.content.observe(viewLifecycleOwner) { exploreAdapter?.items = it } - viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga) - viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged) - viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) { + viewModel.onShowSuggestionsTip.observeEvent(viewLifecycleOwner) { showSuggestionsTip() } } @@ -89,7 +91,7 @@ class ExploreFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) } @@ -138,7 +140,7 @@ class ExploreFragment : } private fun onGridModeChanged(isGrid: Boolean) { - binding.recyclerView.layoutManager = if (isGrid) { + requireViewBinding().recyclerView.layoutManager = if (isGrid) { GridLayoutManager(requireContext(), 4).also { lm -> lm.spanSizeLookup = ExploreGridSpanSizeLookup(checkNotNull(exploreAdapter), lm) } diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreGridSpanSizeLookup.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreGridSpanSizeLookup.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreGridSpanSizeLookup.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreGridSpanSizeLookup.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreMenuProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt similarity index 73% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt index d81472ba3..0f163d92f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -1,29 +1,30 @@ package org.koitharu.kotatsu.explore.ui -import androidx.lifecycle.LiveData -import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.ReversibleHandle -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.explore.ui.model.ExploreItem import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject private const val TIP_SUGGESTIONS = "suggestions" @@ -34,29 +35,28 @@ class ExploreViewModel @Inject constructor( private val exploreRepository: ExploreRepository, ) : BaseViewModel() { - private val gridMode = settings.observeAsStateFlow( + val isGrid = settings.observeAsStateFlow( key = AppSettings.KEY_SOURCES_GRID, scope = viewModelScope + Dispatchers.IO, valueProducer = { isSourcesGridMode }, ) - val onOpenManga = SingleLiveEvent() - val onActionDone = SingleLiveEvent() - val onShowSuggestionsTip = SingleLiveEvent() - val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext) + val onOpenManga = MutableEventFlow() + val onActionDone = MutableEventFlow() + val onShowSuggestionsTip = MutableEventFlow() - val content: LiveData> = isLoading.asFlow().flatMapLatest { loading -> + val content: StateFlow> = isLoading.flatMapLatest { loading -> if (loading) { flowOf(listOf(ExploreItem.Loading)) } else { createContentFlow() } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(ExploreItem.Loading)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(ExploreItem.Loading)) init { launchJob(Dispatchers.Default) { if (!settings.isSuggestionsEnabled && settings.isTipEnabled(TIP_SUGGESTIONS)) { - onShowSuggestionsTip.emitCall(Unit) + onShowSuggestionsTip.call(Unit) } } } @@ -64,7 +64,7 @@ class ExploreViewModel @Inject constructor( fun openRandom() { launchLoadingJob(Dispatchers.Default) { val manga = exploreRepository.findRandomManga(tagsLimit = 8) - onOpenManga.emitCall(manga) + onOpenManga.call(manga) } } @@ -74,7 +74,7 @@ class ExploreViewModel @Inject constructor( val rollback = ReversibleHandle { settings.hiddenSources -= source.name } - onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback)) + onActionDone.call(ReversibleAction(R.string.source_disabled, rollback)) } } @@ -95,7 +95,7 @@ class ExploreViewModel @Inject constructor( } .onStart { emit("") } .map { settings.getMangaSources(includeHidden = false) } - .combine(gridMode) { content, grid -> buildList(content, grid) } + .combine(isGrid) { content, grid -> buildList(content, grid) } private fun buildList(sources: List, isGrid: Boolean): List { val result = ArrayList(sources.size + 3) diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt index 51f5ccc13..f8386cda1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapter.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.explore.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.explore.ui.model.ExploreItem class ExploreAdapter( diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt index a502d6931..bccedb039 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreAdapterDelegates.kt @@ -7,9 +7,15 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.parser.favicon.faviconUri +import org.koitharu.kotatsu.core.ui.image.FaviconFallbackDrawable +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding @@ -17,12 +23,6 @@ import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding import org.koitharu.kotatsu.explore.ui.model.ExploreItem import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.setTextAndVisible -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable fun exploreButtonsAD( clickListener: View.OnClickListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreDiffCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/adapter/ExploreListEventListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt index e9d4603f3..9449ee21a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/model/ExploreItem.kt @@ -18,9 +18,7 @@ sealed interface ExploreItem : ListModel { other as Buttons - if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false - - return true + return isSuggestionsEnabled == other.isSuggestionsEnabled } override fun hashCode(): Int { @@ -40,9 +38,7 @@ sealed interface ExploreItem : ListModel { other as Header if (titleResId != other.titleResId) return false - if (isButtonVisible != other.isButtonVisible) return false - - return true + return isButtonVisible == other.isButtonVisible } override fun hashCode(): Int { @@ -64,9 +60,7 @@ sealed interface ExploreItem : ListModel { other as Source if (source != other.source) return false - if (isGrid != other.isGrid) return false - - return true + return isGrid == other.isGrid } override fun hashCode(): Int { @@ -76,7 +70,6 @@ sealed interface ExploreItem : ListModel { } } - @Deprecated("") class EmptyHint( @DrawableRes icon: Int, @StringRes textPrimary: Int, diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/EntityMapping.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/EntityMapping.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteManga.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index fbea852ab..a21923ed5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -7,12 +7,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.SortOrder import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory @@ -22,7 +23,6 @@ import org.koitharu.kotatsu.favourites.domain.model.Cover import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels -import org.koitharu.kotatsu.utils.ext.mapItems import javax.inject.Inject @Reusable diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt index 3118a4618..dd275369d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/FavouritesActivity.kt @@ -8,12 +8,11 @@ import androidx.core.view.updatePadding import androidx.fragment.app.commit import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID -import kotlin.text.Typography.dagger @AndroidEntryPoint class FavouritesActivity : BaseActivity() { @@ -37,7 +36,7 @@ class FavouritesActivity : BaseActivity() { } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt index c6cbd4be7..c62b36b1c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionCallback.kt @@ -6,7 +6,7 @@ import androidx.appcompat.view.ActionMode import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import com.google.android.material.R as materialR @@ -41,10 +41,12 @@ class CategoriesSelectionCallback( mode.finish() true } + R.id.action_remove -> { confirmDeleteCategories(controller.snapshot(), mode) true } + else -> false } } @@ -61,4 +63,4 @@ class CategoriesSelectionCallback( mode.finish() }.show() } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt index ebeaf648a..3ee6833f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/CategoriesSelectionDecoration.kt @@ -9,10 +9,10 @@ import android.view.View import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.util.ext.getItem +import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel -import org.koitharu.kotatsu.utils.ext.getItem -import org.koitharu.kotatsu.utils.ext.getThemeColor import com.google.android.material.R as materialR class CategoriesSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { @@ -22,7 +22,7 @@ class CategoriesSelectionDecoration(context: Context) : AbstractSelectionItemDec private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED) private val fillColor = ColorUtils.setAlphaComponent( ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f), - 0x74 + 0x74, ) private val padding = context.resources.getDimension(R.dimen.grid_spacing_outer) @@ -54,4 +54,4 @@ class CategoriesSelectionDecoration(context: Context) : AbstractSelectionItemDec paint.style = Paint.Style.STROKE canvas.drawRoundRect(bounds, radius, radius, paint) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt index 3be5f542f..e72e050fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt @@ -20,10 +20,13 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.list.ListSelectionController import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.favourites.ui.FavouritesActivity import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoriesAdapter @@ -31,7 +34,6 @@ import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEdit import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import javax.inject.Inject @AndroidEntryPoint @@ -61,17 +63,17 @@ class FavouriteCategoriesActivity : activity = this, decoration = CategoriesSelectionDecoration(this), registryOwner = this, - callback = CategoriesSelectionCallback(binding.recyclerView, viewModel), + callback = CategoriesSelectionCallback(viewBinding.recyclerView, viewModel), ) - binding.buttonDone.setOnClickListener(this) - selectionController.attachToRecyclerView(binding.recyclerView) - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.adapter = adapter - binding.fabAdd.setOnClickListener(this) + viewBinding.buttonDone.setOnClickListener(this) + selectionController.attachToRecyclerView(viewBinding.recyclerView) + viewBinding.recyclerView.setHasFixedSize(true) + viewBinding.recyclerView.adapter = adapter + viewBinding.fabAdd.setOnClickListener(this) onBackPressedDispatcher.addCallback(exitReorderModeCallback) viewModel.detalizedCategories.observe(this, ::onCategoriesChanged) - viewModel.onError.observe(this, SnackbarErrorObserver(binding.recyclerView, null)) + viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.isInReorderMode.observe(this, ::onReorderModeChanged) } @@ -110,7 +112,7 @@ class FavouriteCategoriesActivity : } val intent = FavouritesActivity.newIntent(this, item) val options = scaleUpActivityOptionsOf(view) - startActivity(intent, options.toBundle()) + startActivity(intent, options) } override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean { @@ -126,16 +128,16 @@ class FavouriteCategoriesActivity : override fun onEmptyActionClick() = Unit override fun onWindowInsetsChanged(insets: Insets) { - binding.fabAdd.updateLayoutParams { + viewBinding.fabAdd.updateLayoutParams { rightMargin = topMargin + insets.right leftMargin = topMargin + insets.left bottomMargin = topMargin + insets.bottom } - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( bottom = insets.bottom, ) } @@ -149,21 +151,21 @@ class FavouriteCategoriesActivity : val transition = Fade().apply { duration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() } - TransitionManager.beginDelayedTransition(binding.toolbar, transition) + TransitionManager.beginDelayedTransition(viewBinding.toolbar, transition) reorderHelper?.attachToRecyclerView(null) reorderHelper = if (isReorderMode) { selectionController.clear() - binding.fabAdd.hide() + viewBinding.fabAdd.hide() ItemTouchHelper(ReorderHelperCallback()).apply { - attachToRecyclerView(binding.recyclerView) + attachToRecyclerView(viewBinding.recyclerView) } } else { - binding.fabAdd.show() + viewBinding.fabAdd.show() null } - binding.recyclerView.isNestedScrollingEnabled = !isReorderMode + viewBinding.recyclerView.isNestedScrollingEnabled = !isReorderMode invalidateOptionsMenu() - binding.buttonDone.isVisible = isReorderMode + viewBinding.buttonDone.isVisible = isReorderMode exitReorderModeCallback.isEnabled = isReorderMode } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt index 7819d0112..f85ff122e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt @@ -1,10 +1,10 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.recyclerview.widget.RecyclerView -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener interface FavouriteCategoriesListListener : OnListItemClickListener { fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 6c8d48798..f7a397df0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -1,22 +1,22 @@ package org.koitharu.kotatsu.favourites.ui.categories -import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.requireValue import java.util.Collections import javax.inject.Inject @@ -27,23 +27,11 @@ class FavouritesCategoriesViewModel @Inject constructor( ) : BaseViewModel() { private var reorderJob: Job? = null - private val isReorder = MutableStateFlow(false) - - val isInReorderMode = isReorder.asLiveData(viewModelScope.coroutineContext) - - val allCategories = repository.observeCategories() - .mapItems { - CategoryListModel( - mangaCount = 0, - covers = listOf(), - category = it, - isReorderMode = false, - ) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + val isInReorderMode = MutableStateFlow(false) val detalizedCategories = combine( repository.observeCategoriesWithCovers(), - isReorder, + isInReorderMode, ) { list, reordering -> list.map { (category, covers) -> CategoryListModel( @@ -62,7 +50,7 @@ class FavouritesCategoriesViewModel @Inject constructor( ), ) } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) fun deleteCategory(id: Long) { launchJob { @@ -80,12 +68,12 @@ class FavouritesCategoriesViewModel @Inject constructor( settings.isAllFavouritesVisible = isVisible } - fun isInReorderMode(): Boolean = isReorder.value + fun isInReorderMode(): Boolean = isInReorderMode.value - fun isEmpty(): Boolean = detalizedCategories.value?.none { it is CategoryListModel } ?: true + fun isEmpty(): Boolean = detalizedCategories.value.none { it is CategoryListModel } fun setReorderMode(isReorderMode: Boolean) { - isReorder.value = isReorderMode + isInReorderMode.value = isReorderMode } fun reorderCategories(oldPos: Int, newPos: Int) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt index 846d0aabb..9ab263483 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt @@ -15,15 +15,15 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.getAnimationDuration -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source fun categoryAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt similarity index 68% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt index a9a99305b..3b055ddff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt @@ -16,16 +16,18 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.util.DefaultTextWatcher import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.model.titleRes +import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getSerializableCompat +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.getSerializableCompat -import org.koitharu.kotatsu.utils.ext.setChecked import com.google.android.material.R as materialR @AndroidEntryPoint @@ -46,16 +48,16 @@ class FavouritesCategoryEditActivity : setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } initSortSpinner() - binding.buttonDone.setOnClickListener(this) - binding.editName.addTextChangedListener(this) - afterTextChanged(binding.editName.text) + viewBinding.buttonDone.setOnClickListener(this) + viewBinding.editName.addTextChangedListener(this) + afterTextChanged(viewBinding.editName.text) - viewModel.onSaved.observe(this) { finishAfterTransition() } + viewModel.onSaved.observeEvent(this) { finishAfterTransition() } viewModel.category.observe(this, ::onCategoryChanged) viewModel.isLoading.observe(this, ::onLoadingStateChanged) - viewModel.onError.observe(this, ::onError) + viewModel.onError.observeEvent(this, ::onError) viewModel.isTrackerEnabled.observe(this) { - binding.switchTracker.isVisible = it + viewBinding.switchTracker.isVisible = it } } @@ -75,27 +77,27 @@ class FavouritesCategoryEditActivity : override fun onClick(v: View) { when (v.id) { R.id.button_done -> viewModel.save( - title = binding.editName.text?.toString()?.trim().orEmpty(), + title = viewBinding.editName.text?.toString()?.trim().orEmpty(), sortOrder = getSelectedSortOrder(), - isTrackerEnabled = binding.switchTracker.isChecked, - isVisibleOnShelf = binding.switchShelf.isChecked, + isTrackerEnabled = viewBinding.switchTracker.isChecked, + isVisibleOnShelf = viewBinding.switchShelf.isChecked, ) } } override fun afterTextChanged(s: Editable?) { - binding.buttonDone.isEnabled = !s.isNullOrBlank() + viewBinding.buttonDone.isEnabled = !s.isNullOrBlank() } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) - binding.scrollView.updatePadding( + viewBinding.scrollView.updatePadding( bottom = insets.bottom, ) - binding.toolbar.updateLayoutParams { + viewBinding.toolbar.updateLayoutParams { topMargin = insets.top } } @@ -109,40 +111,40 @@ class FavouritesCategoryEditActivity : if (selectedSortOrder != null) { return } - binding.editName.setText(category?.title) + viewBinding.editName.setText(category?.title) selectedSortOrder = category?.order val sortText = getString((category?.order ?: SortOrder.NEWEST).titleRes) - binding.editSort.setText(sortText, false) - binding.switchTracker.setChecked(category?.isTrackingEnabled ?: true, false) - binding.switchShelf.setChecked(category?.isVisibleInLibrary ?: true, false) + viewBinding.editSort.setText(sortText, false) + viewBinding.switchTracker.setChecked(category?.isTrackingEnabled ?: true, false) + viewBinding.switchShelf.setChecked(category?.isVisibleInLibrary ?: true, false) } private fun onError(e: Throwable) { - binding.textViewError.text = e.getDisplayMessage(resources) - binding.textViewError.isVisible = true + viewBinding.textViewError.text = e.getDisplayMessage(resources) + viewBinding.textViewError.isVisible = true } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.editSort.isEnabled = !isLoading - binding.editName.isEnabled = !isLoading - binding.switchTracker.isEnabled = !isLoading - binding.switchShelf.isEnabled = !isLoading + viewBinding.editSort.isEnabled = !isLoading + viewBinding.editName.isEnabled = !isLoading + viewBinding.switchTracker.isEnabled = !isLoading + viewBinding.switchShelf.isEnabled = !isLoading if (isLoading) { - binding.textViewError.isVisible = false + viewBinding.textViewError.isVisible = false } } private fun initSortSpinner() { val entries = FavouriteCategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) } val adapter = SortAdapter(this, entries) - binding.editSort.setAdapter(adapter) - binding.editSort.onItemClickListener = this + viewBinding.editSort.setAdapter(adapter) + viewBinding.editSort.onItemClickListener = this } private fun getSelectedSortOrder(): SortOrder { selectedSortOrder?.let { return it } val entries = FavouriteCategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) } - val index = entries.indexOf(binding.editSort.text.toString()) + val index = entries.indexOf(viewBinding.editSort.text.toString()) return FavouriteCategoriesActivity.SORT_ORDERS.getOrNull(index) ?: SortOrder.NEWEST } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt similarity index 68% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt index 8e84a52c1..2b32a3d5c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt @@ -1,20 +1,23 @@ package org.koitharu.kotatsu.favourites.ui.categories.edit -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.EXTRA_ID import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_ID import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.emitValue import javax.inject.Inject @HiltViewModel @@ -26,22 +29,20 @@ class FavouritesCategoryEditViewModel @Inject constructor( private val categoryId = savedStateHandle[EXTRA_ID] ?: NO_ID - val onSaved = SingleLiveEvent() - val category = MutableLiveData() + val onSaved = MutableEventFlow() + val category = MutableStateFlow(null) - val isTrackerEnabled = liveData(viewModelScope.coroutineContext + Dispatchers.Default) { + val isTrackerEnabled = flow { emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources) - } + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) init { launchLoadingJob(Dispatchers.Default) { - category.emitValue( - if (categoryId != NO_ID) { - repository.getCategory(categoryId) - } else { - null - }, - ) + category.value = if (categoryId != NO_ID) { + repository.getCategory(categoryId) + } else { + null + } } } @@ -58,7 +59,7 @@ class FavouritesCategoryEditViewModel @Inject constructor( } else { repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled, isVisibleOnShelf) } - onSaved.emitCall(Unit) + onSaved.call(Unit) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt similarity index 52% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt index 93bf67e1d..7d5be8b7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesSheet.kt @@ -2,51 +2,48 @@ package org.koitharu.kotatsu.favourites.ui.categories.select import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.appcompat.widget.Toolbar 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.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding -import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint -class FavouriteCategoriesBottomSheet : - BaseBottomSheet(), - OnListItemClickListener, - View.OnClickListener, - Toolbar.OnMenuItemClickListener { +class FavouriteCategoriesSheet : + BaseAdaptiveSheet(), + OnListItemClickListener { private val viewModel: MangaCategoriesViewModel by viewModels() private var adapter: MangaCategoriesAdapter? = null - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated( + binding: SheetFavoriteCategoriesBinding, + savedInstanceState: Bundle?, + ) { + super.onViewBindingCreated(binding, savedInstanceState) adapter = MangaCategoriesAdapter(this) binding.recyclerViewCategories.adapter = adapter - binding.buttonDone.setOnClickListener(this) - binding.headerBar.toolbar.setOnMenuItemClickListener(this) - viewModel.content.observe(viewLifecycleOwner, this::onContentChanged) - viewModel.onError.observe(viewLifecycleOwner, ::onError) + viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) } override fun onDestroyView() { @@ -54,25 +51,11 @@ class FavouriteCategoriesBottomSheet : super.onDestroyView() } - override fun onClick(v: View) { - when (v.id) { - R.id.button_done -> dismiss() - } - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext())) - else -> return false - } - return true - } - override fun onItemClick(item: MangaCategoryItem, view: View) { viewModel.setChecked(item.id, !item.isChecked) } - private fun onContentChanged(categories: List) { + private fun onContentChanged(categories: List) { adapter?.items = categories } @@ -87,11 +70,17 @@ class FavouriteCategoriesBottomSheet : fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga)) - fun show(fm: FragmentManager, manga: Collection) = FavouriteCategoriesBottomSheet().withArgs(1) { - putParcelableArrayList( - KEY_MANGA_LIST, - manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) }, - ) - }.show(fm, TAG) + fun show(fm: FragmentManager, manga: Collection) = + FavouriteCategoriesSheet().withArgs(1) { + putParcelableArrayList( + KEY_MANGA_LIST, + manga.mapTo(ArrayList(manga.size)) { + ParcelableManga( + it, + withChapters = false, + ) + }, + ) + }.show(fm, TAG) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt index 791a79787..5f6566ceb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt @@ -4,14 +4,20 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem -import org.koitharu.kotatsu.utils.asFlowLiveData +import org.koitharu.kotatsu.list.ui.model.ListModel import javax.inject.Inject @HiltViewModel @@ -20,20 +26,24 @@ class MangaCategoriesViewModel @Inject constructor( private val favouritesRepository: FavouritesRepository, ) : BaseViewModel() { - private val manga = requireNotNull(savedStateHandle.get>(KEY_MANGA_LIST)).map { it.manga } + private val manga = savedStateHandle.require>(KEY_MANGA_LIST).map { it.manga } + private val header = CategoriesHeaderItem() - val content = combine( + val content: StateFlow> = combine( favouritesRepository.observeCategories(), observeCategoriesIds(), ) { all, checked -> - all.map { - MangaCategoryItem( - id = it.id, - name = it.title, - isChecked = it.id in checked, - ) + buildList(all.size + 1) { + add(header) + all.mapTo(this) { + MangaCategoryItem( + id = it.id, + name = it.title, + isChecked = it.id in checked, + ) + } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) fun setChecked(categoryId: Long, isChecked: Boolean) { launchJob(Dispatchers.Default) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt new file mode 100644 index 000000000..cc878a1ac --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.favourites.ui.categories.select.adapter + +import android.view.View +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding +import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun categoriesHeaderAD() = adapterDelegateViewBinding( + { inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) }, +) { + + val onClickListener = View.OnClickListener { v -> + val intent = when (v.id) { + R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context) + R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context) + else -> return@OnClickListener + } + v.context.startActivity(intent) + } + + binding.buttonCreate.setOnClickListener(onClickListener) + binding.buttonManage.setOnClickListener(onClickListener) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt new file mode 100644 index 000000000..e0578cf56 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.favourites.ui.categories.select.adapter + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem +import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel + +class MangaCategoriesAdapter( + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(mangaCategoryAD(clickListener)) + .addDelegate(categoriesHeaderAD()) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ListModel, + newItem: ListModel, + ): Boolean = when { + oldItem is MangaCategoryItem && newItem is MangaCategoryItem -> oldItem.id == newItem.id + oldItem is CategoriesHeaderItem && newItem is CategoriesHeaderItem -> oldItem == newItem + else -> false + } + + override fun areContentsTheSame( + oldItem: ListModel, + newItem: ListModel, + ): Boolean = oldItem == newItem + + override fun getChangePayload( + oldItem: ListModel, + newItem: ListModel, + ): Any? { + if (oldItem is MangaCategoryItem && newItem is MangaCategoryItem && oldItem.isChecked != newItem.isChecked) { + return newItem.isChecked + } + return super.getChangePayload(oldItem, newItem) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt similarity index 66% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt index c9ce1e8b2..05f4b7505 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt @@ -1,14 +1,15 @@ package org.koitharu.kotatsu.favourites.ui.categories.select.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem +import org.koitharu.kotatsu.list.ui.model.ListModel fun mangaCategoryAD( - clickListener: OnListItemClickListener -) = adapterDelegateViewBinding( - { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) } + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }, ) { itemView.setOnClickListener { @@ -21,4 +22,4 @@ fun mangaCategoryAD( isChecked = item.isChecked } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt new file mode 100644 index 000000000..cde85e768 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/CategoriesHeaderItem.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.favourites.ui.categories.select.model + +import org.koitharu.kotatsu.list.ui.model.ListModel + +class CategoriesHeaderItem : ListModel { + + override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java +} diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt similarity index 58% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt index 95447a2af..721176233 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/model/MangaCategoryItem.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.favourites.ui.categories.select.model +import org.koitharu.kotatsu.list.ui.model.ListModel + data class MangaCategoryItem( val id: Long, val name: String, - val isChecked: Boolean -) \ No newline at end of file + val isChecked: Boolean, +) : ListModel diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index d37f33c56..9e22b6a62 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -9,13 +9,15 @@ import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController -import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.model.titleRes +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener { @@ -24,10 +26,10 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis override val isSwipeRefreshEnabled = false - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) if (viewModel.categoryId != NO_ID) { - addMenuProvider(FavouritesListMenuProvider(view.context, viewModel)) + addMenuProvider(FavouritesListMenuProvider(binding.root.context, viewModel)) } viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt index 85e49c294..4300d3e05 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt @@ -7,7 +7,7 @@ import android.view.MenuItem import androidx.core.view.MenuProvider import androidx.core.view.forEach import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.core.ui.model.titleRes import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.parsers.model.SortOrder diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index b6007cd6e..dea73f94f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -1,24 +1,25 @@ package org.koitharu.kotatsu.favourites.ui.list -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.util.ReversibleAction -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -26,29 +27,25 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel class FavouritesListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: FavouritesRepository, - private val trackingRepository: TrackingRepository, - private val historyRepository: HistoryRepository, - private val settings: AppSettings, - private val tagHighlighter: MangaTagHighlighter, + private val listExtraProvider: ListExtraProvider, + settings: AppSettings, downloadScheduler: DownloadWorker.Scheduler, -) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { +) : MangaListViewModel(settings, downloadScheduler) { val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID - val sortOrder: LiveData = if (categoryId == NO_ID) { - MutableLiveData(null) + val sortOrder: StateFlow = if (categoryId == NO_ID) { + MutableStateFlow(null) } else { repository.observeCategory(categoryId) .map { it?.order } - .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) } override val content = combine( @@ -57,7 +54,7 @@ class FavouritesListViewModel @Inject constructor( } else { repository.observeAll(categoryId) }, - listModeFlow, + listMode, ) { list, mode -> when { list.isEmpty() -> listOf( @@ -73,11 +70,11 @@ class FavouritesListViewModel @Inject constructor( ), ) - else -> list.toUi(mode, this, tagHighlighter) + else -> list.toUi(mode, listExtraProvider) } }.catch { emit(listOf(it.toErrorState(canRetry = false))) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) override fun onRefresh() = Unit @@ -93,7 +90,7 @@ class FavouritesListViewModel @Inject constructor( } else { repository.removeFromCategory(categoryId, ids) } - onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle)) + onActionDone.call(ReversibleAction(R.string.removed_from_favourites, handle)) } } @@ -105,20 +102,4 @@ class FavouritesListViewModel @Inject constructor( repository.setCategoryOrder(categoryId, order) } } - - override suspend fun getCounter(mangaId: Long): Int { - return if (settings.isTrackerEnabled) { - trackingRepository.getNewChaptersCount(mangaId) - } else { - 0 - } - } - - override suspend fun getProgress(mangaId: Long): Float { - return if (settings.isReadingIndicatorsEnabled) { - historyRepository.getProgress(mangaId) - } else { - PROGRESS_NONE - } - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt new file mode 100644 index 000000000..74d7459fc --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.filter.ui + +import android.content.Context +import androidx.recyclerview.widget.AsyncListDiffer.ListListener +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.filter.ui.model.FilterItem +import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD +import org.koitharu.kotatsu.list.ui.model.ListModel + +class FilterAdapter( + listener: OnFilterChangedListener, + listListener: ListListener, +) : AsyncListDifferDelegationAdapter(FilterDiffCallback()), FastScroller.SectionIndexer { + + init { + delegatesManager + .addDelegate(filterSortDelegate(listener)) + .addDelegate(filterTagDelegate(listener)) + .addDelegate(listSimpleHeaderAD()) + .addDelegate(loadingStateAD()) + .addDelegate(loadingFooterAD()) + .addDelegate(filterErrorDelegate()) + differ.addListListener(listListener) + } + + override fun getSectionText(context: Context, position: Int): CharSequence? { + val list = items + for (i in (0..position).reversed()) { + val item = list.getOrNull(i) ?: continue + if (item is FilterItem.Tag) { + return item.tag.title.firstOrNull()?.toString() + } + } + return null + } + + companion object { + + const val ITEM_TYPE_HEADER = 0 + const val ITEM_TYPE_SORT = 1 + const val ITEM_TYPE_TAG = 2 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt new file mode 100644 index 000000000..c2125070c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.filter.ui + +import android.widget.TextView +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.model.titleRes +import org.koitharu.kotatsu.core.util.ext.setChecked +import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding +import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding +import org.koitharu.kotatsu.filter.ui.model.FilterItem +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun filterSortDelegate( + listener: OnFilterChangedListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) }, +) { + + itemView.setOnClickListener { + listener.onSortItemClick(item) + } + + bind { payloads -> + binding.root.setText(item.order.titleRes) + binding.root.setChecked(item.isSelected, payloads.isNotEmpty()) + } +} + +fun filterTagDelegate( + listener: OnFilterChangedListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, +) { + + itemView.setOnClickListener { + listener.onTagItemClick(item) + } + + bind { payloads -> + binding.root.text = item.tag.title + binding.root.setChecked(item.isChecked, payloads.isNotEmpty()) + } +} + +fun filterErrorDelegate() = adapterDelegate(R.layout.item_sources_empty) { + + bind { + (itemView as TextView).setText(item.textResId) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt similarity index 56% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 96fb630ae..bff70420b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -1,48 +1,82 @@ -package org.koitharu.kotatsu.list.ui.filter +package org.koitharu.kotatsu.filter.ui import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import kotlinx.coroutines.CoroutineScope +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.ViewModelLifecycle +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.ext.lifecycleScope +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel +import org.koitharu.kotatsu.filter.ui.model.FilterItem +import org.koitharu.kotatsu.filter.ui.model.FilterState +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment +import org.koitharu.kotatsu.search.domain.MangaSearchRepository +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.text.Collator +import java.util.LinkedList import java.util.Locale import java.util.TreeSet +import javax.inject.Inject -class FilterCoordinator( - private val repository: RemoteMangaRepository, +@ViewModelScoped +class FilterCoordinator @Inject constructor( + savedStateHandle: SavedStateHandle, + mangaRepositoryFactory: MangaRepository.Factory, dataRepository: MangaDataRepository, - private val coroutineScope: CoroutineScope, -) : OnFilterChangedListener { + private val searchRepository: MangaSearchRepository, + lifecycle: ViewModelLifecycle, +) : FilterOwner { + private val coroutineScope = lifecycle.lifecycleScope + private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE)) private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet())) private var searchQuery = MutableStateFlow("") - private val localTagsDeferred = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) { + private val localTags = SuspendLazy { dataRepository.findTags(repository.source) } private var availableTagsDeferred = loadTagsAsync() - val items: LiveData> = getItemsFlow() - .asFlowLiveData(coroutineScope.coroutineContext + Dispatchers.Default, listOf(FilterItem.Loading)) + override val filterItems: StateFlow> = getItemsFlow() + .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState)) + + override val header: StateFlow = getHeaderFlow().stateIn( + scope = coroutineScope + Dispatchers.Default, + started = SharingStarted.Lazily, + initialValue = FilterHeaderModel(emptyList(), repository.defaultSortOrder, false), + ) init { observeState() } + override fun applyFilter(tags: Set) { + setTags(tags) + } + override fun onSortItemClick(item: FilterItem.Sort) { currentState.update { oldValue -> FilterState(item.order, oldValue.tags) @@ -88,6 +122,14 @@ class FilterCoordinator( searchQuery.value = query } + private fun getHeaderFlow() = combine( + observeState(), + observeAvailableTags(), + ) { state, available -> + val chips = createChipsList(state, available.orEmpty()) + FilterHeaderModel(chips, state.sortOrder, state.tags.isNotEmpty()) + } + private fun getItemsFlow() = combine( getTagsAsFlow(), currentState, @@ -97,7 +139,7 @@ class FilterCoordinator( } private fun getTagsAsFlow() = flow { - val localTags = localTagsDeferred.await() + val localTags = localTags.get() emit(TagsWrapper(localTags, isLoading = true, isError = false)) val remoteTags = tryLoadTags() if (remoteTags == null) { @@ -107,24 +149,66 @@ class FilterCoordinator( } } + private suspend fun createChipsList( + filterState: FilterState, + availableTags: Set, + ): List { + val selectedTags = filterState.tags.toMutableSet() + var tags = searchRepository.getTagsSuggestion("", 6, repository.source) + if (tags.isEmpty()) { + tags = availableTags.take(6) + } + if (tags.isEmpty() && selectedTags.isEmpty()) { + return emptyList() + } + val result = LinkedList() + for (tag in tags) { + val model = ChipsView.ChipModel( + tint = 0, + title = tag.title, + icon = 0, + isCheckable = true, + isChecked = selectedTags.remove(tag), + data = tag, + ) + if (model.isChecked) { + result.addFirst(model) + } else { + result.addLast(model) + } + } + for (tag in selectedTags) { + val model = ChipsView.ChipModel( + tint = 0, + title = tag.title, + icon = 0, + isCheckable = true, + isChecked = true, + data = tag, + ) + result.addFirst(model) + } + return result + } + @WorkerThread private fun buildFilterList( allTags: TagsWrapper, state: FilterState, query: String, - ): List { + ): List { val sortOrders = repository.sortOrders.sortedBy { it.ordinal } val tags = mergeTags(state.tags, allTags.tags).toList() - val list = ArrayList(tags.size + sortOrders.size + 3) + val list = ArrayList(tags.size + sortOrders.size + 3) if (query.isEmpty()) { if (sortOrders.isNotEmpty()) { - list.add(FilterItem.Header(R.string.sort_order, 0)) + list.add(ListHeader(R.string.sort_order, 0, null)) sortOrders.mapTo(list) { FilterItem.Sort(it, isSelected = it == state.sortOrder) } } if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) { - list.add(FilterItem.Header(R.string.genres, state.tags.size)) + list.add(ListHeader(R.string.genres, 0, null)) tags.mapTo(list) { FilterItem.Tag(it, isChecked = it in state.tags) } @@ -132,7 +216,7 @@ class FilterCoordinator( if (allTags.isError) { list.add(FilterItem.Error(R.string.filter_load_error)) } else if (allTags.isLoading) { - list.add(FilterItem.Loading) + list.add(LoadingFooter()) } } else { tags.mapNotNullTo(list) { @@ -188,9 +272,7 @@ class FilterCoordinator( if (tags != other.tags) return false if (isLoading != other.isLoading) return false - if (isError != other.isError) return false - - return true + return isError == other.isError } override fun hashCode(): Int { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterDiffCallback.kt new file mode 100644 index 000000000..d3319c431 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterDiffCallback.kt @@ -0,0 +1,52 @@ +package org.koitharu.kotatsu.filter.ui + +import androidx.recyclerview.widget.DiffUtil +import org.koitharu.kotatsu.filter.ui.model.FilterItem +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel + +class FilterDiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return when { + oldItem === newItem -> true + oldItem.javaClass != newItem.javaClass -> false + oldItem is ListHeader && newItem is ListHeader -> { + oldItem == newItem + } + + oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { + oldItem.tag == newItem.tag + } + + oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { + oldItem.order == newItem.order + } + + oldItem is FilterItem.Error && newItem is FilterItem.Error -> { + oldItem.textResId == newItem.textResId + } + + else -> false + } + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { + val hasPayload = when { + oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> { + oldItem.isChecked != newItem.isChecked + } + + oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> { + oldItem.isSelected != newItem.isSelected + } + + else -> false + } + return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt new file mode 100644 index 000000000..4f50e9df5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -0,0 +1,71 @@ +package org.koitharu.kotatsu.filter.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.view.isVisible +import com.google.android.material.chip.Chip +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding +import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel +import org.koitharu.kotatsu.filter.ui.model.FilterItem +import org.koitharu.kotatsu.parsers.model.MangaTag +import com.google.android.material.R as materialR + +class FilterHeaderFragment : BaseFragment(), ChipsView.OnChipClickListener { + + private val owner by lazy(LazyThreadSafetyMode.NONE) { + FilterOwner.from(requireActivity()) + } + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding { + return FragmentFilterHeaderBinding.inflate(inflater, container, false) + } + + override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + binding.chipsTags.onChipClickListener = this + owner.header.observe(viewLifecycleOwner, ::onDataChanged) + } + + override fun onWindowInsetsChanged(insets: Insets) = Unit + + override fun onChipClick(chip: Chip, data: Any?) { + val tag = data as? MangaTag + if (tag == null) { + FilterSheetFragment.show(parentFragmentManager) + } else { + owner.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked)) + } + } + + private fun onDataChanged(header: FilterHeaderModel) { + val binding = viewBinding ?: return + val chips = header.chips + if (chips.isEmpty()) { + binding.chipsTags.setChips(emptyList()) + binding.root.isVisible = false + return + } + if (binding.root.context.isAnimationsEnabled) { + binding.scrollView.smoothScrollTo(0, 0) + } else { + binding.scrollView.scrollTo(0, 0) + } + binding.chipsTags.setChips(header.chips + moreTagsChip()) + binding.root.isVisible = true + } + + private fun moreTagsChip() = ChipsView.ChipModel( + tint = 0, + title = getString(R.string.more), + icon = materialR.drawable.abc_ic_menu_overflow_material, + isCheckable = false, + isChecked = false, + ) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterOwner.kt new file mode 100644 index 000000000..b302e7692 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterOwner.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.filter.ui + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import kotlinx.coroutines.flow.StateFlow +import org.koitharu.kotatsu.core.util.ext.values +import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.MangaTag + +interface FilterOwner : OnFilterChangedListener { + + val filterItems: StateFlow> + + val header: StateFlow + + fun applyFilter(tags: Set) + + companion object { + + fun from(activity: FragmentActivity): FilterOwner { + for (f in activity.supportFragmentManager.fragments) { + return find(f) ?: continue + } + error("Cannot find FilterOwner") + } + + fun find(fragment: Fragment): FilterOwner? { + return fragment.viewModelStore.values.firstNotNullOfOrNull { it as? FilterOwner } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterSheetFragment.kt new file mode 100644 index 000000000..c9734d9c7 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterSheetFragment.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.filter.ui + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.LinearLayoutManager +import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior +import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.databinding.SheetFilterBinding +import org.koitharu.kotatsu.list.ui.model.ListModel + +class FilterSheetFragment : + BaseAdaptiveSheet(), + AdaptiveSheetCallback, + AsyncListDiffer.ListListener { + + private val owner by lazy(LazyThreadSafetyMode.NONE) { + FilterOwner.from(requireActivity()) + } + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { + return SheetFilterBinding.inflate(inflater, container, false) + } + + override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + addSheetCallback(this) + val adapter = FilterAdapter(owner, this) + binding.recyclerView.adapter = adapter + owner.filterItems.observe(viewLifecycleOwner, adapter::setItems) + + if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + binding.recyclerView.scrollIndicators = 0 + } + } + + override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { + if (currentList.size > previousList.size && view != null) { + (requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) + } + } + + override fun onStateChanged(sheet: View, newState: Int) { + viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED + } + + companion object { + + private const val TAG = "FilterBottomSheet" + + fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt similarity index 56% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt index a28596c9f..bf3f15f93 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt @@ -1,8 +1,10 @@ -package org.koitharu.kotatsu.list.ui.filter +package org.koitharu.kotatsu.filter.ui + +import org.koitharu.kotatsu.filter.ui.model.FilterItem interface OnFilterChangedListener { fun onSortItemClick(item: FilterItem.Sort) fun onTagItemClick(item: FilterItem.Tag) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt similarity index 59% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt index 0f053bfd6..cf9dcd834 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt @@ -1,25 +1,28 @@ -package org.koitharu.kotatsu.list.ui.model +package org.koitharu.kotatsu.filter.ui.model -import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.SortOrder -class ListHeader2( +class FilterHeaderModel( val chips: Collection, val sortOrder: SortOrder?, val hasSelectedTags: Boolean, ) : ListModel { + val textSummary: String + get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString() + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as ListHeader2 + other as FilterHeaderModel if (chips != other.chips) return false - if (sortOrder != other.sortOrder) return false + return sortOrder == other.sortOrder // Not need to check hasSelectedTags - return true } override fun hashCode(): Int { @@ -27,4 +30,4 @@ class ListHeader2( result = 31 * result + (sortOrder?.hashCode() ?: 0) return result } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt new file mode 100644 index 000000000..a2ad2cddb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt @@ -0,0 +1,71 @@ +package org.koitharu.kotatsu.filter.ui.model + +import androidx.annotation.StringRes +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder + +sealed interface FilterItem : ListModel { + + class Sort( + val order: SortOrder, + val isSelected: Boolean, + ) : FilterItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Sort + + if (order != other.order) return false + return isSelected == other.isSelected + } + + override fun hashCode(): Int { + var result = order.hashCode() + result = 31 * result + isSelected.hashCode() + return result + } + } + + class Tag( + val tag: MangaTag, + val isChecked: Boolean, + ) : FilterItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Tag + + if (tag != other.tag) return false + return isChecked == other.isChecked + } + + override fun hashCode(): Int { + var result = tag.hashCode() + result = 31 * result + isChecked.hashCode() + return result + } + } + + class Error( + @StringRes val textResId: Int, + ) : FilterItem { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Error + + return textResId == other.textResId + } + + override fun hashCode(): Int { + return textResId + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt index d9e387b89..b4ab41c7a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.list.ui.filter +package org.koitharu.kotatsu.filter.ui.model import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder @@ -15,9 +15,7 @@ class FilterState( other as FilterState if (sortOrder != other.sortOrder) return false - if (tags != other.tags) return false - - return true + return tags == other.tags } override fun hashCode(): Int { @@ -25,4 +23,4 @@ class FilterState( result = 31 * result + tags.hashCode() return result } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/EntityMapping.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/data/EntityMapping.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index be2c5f7e9..0e033ddd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -1,6 +1,10 @@ package org.koitharu.kotatsu.history.data -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity @@ -20,6 +24,10 @@ abstract class HistoryDao { @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC") abstract fun observeAll(): Flow> + @Transaction + @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit") + abstract fun observeAll(limit: Int): Flow> + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)") abstract suspend fun findAllManga(): List diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 1e84585de..3cd576e87 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -1,15 +1,12 @@ -package org.koitharu.kotatsu.history.domain +package org.koitharu.kotatsu.history.data import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity @@ -18,15 +15,14 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTag import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.history.data.HistoryEntity -import org.koitharu.kotatsu.history.data.toMangaHistory +import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.mapItems +import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.ext.mapItems import javax.inject.Inject const val PROGRESS_NONE = -1f @@ -55,6 +51,12 @@ class HistoryRepository @Inject constructor( } } + fun observeAll(limit: Int): Flow> { + return db.historyDao.observeAll(limit).mapItems { + it.manga.toManga(it.tags.toMangaTags()) + } + } + fun observeAllWithHistory(): Flow> { return db.historyDao.observeAll().mapItems { MangaWithHistory( @@ -161,18 +163,6 @@ class HistoryRepository @Inject constructor( .distinctUntilChanged() } - fun observeShouldSkip(mangaFlow: Flow): Flow { - return mangaFlow - .distinctUntilChangedBy { it?.isNsfw } - .flatMapLatest { m -> - if (m != null) { - observeShouldSkip(m) - } else { - settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled } - } - } - } - private suspend fun recover(ids: Collection) { db.withTransaction { for (id in ids) { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryWithManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/history/data/HistoryWithManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryWithManga.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryUpdateUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryUpdateUseCase.kt new file mode 100644 index 000000000..452d0368f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryUpdateUseCase.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.history.domain + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import javax.inject.Inject + +class HistoryUpdateUseCase @Inject constructor( + private val historyRepository: HistoryRepository, +) { + + suspend operator fun invoke(manga: Manga, readerState: ReaderState, percent: Float) { + historyRepository.addOrUpdate( + manga = manga, + chapterId = readerState.chapterId, + page = readerState.page, + scroll = readerState.scroll, + percent = percent, + ) + } + + fun invokeAsync( + manga: Manga, + readerState: ReaderState, + percent: Float + ) = processLifecycleScope.launch(Dispatchers.Default) { + runCatchingCancellable { + invoke(manga, readerState, percent) + }.onFailure { + it.printStackTraceDebug() + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/MangaWithHistory.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/MangaWithHistory.kt index 611dd1ded..5a7e26897 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/MangaWithHistory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/model/MangaWithHistory.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.history.domain +package org.koitharu.kotatsu.history.domain.model import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.parsers.model.Manga @@ -6,4 +6,4 @@ import org.koitharu.kotatsu.parsers.model.Manga data class MangaWithHistory( val manga: Manga, val history: MangaHistory -) \ No newline at end of file +) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryActivity.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryActivity.kt index f7e6b58ec..e9b2dcda1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryActivity.kt @@ -9,10 +9,9 @@ import androidx.fragment.app.commit import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner -import kotlin.text.Typography.dagger @AndroidEntryPoint class HistoryActivity : @@ -20,7 +19,7 @@ class HistoryActivity : AppBarOwner { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -37,7 +36,7 @@ class HistoryActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt index 4f0805690..a17e0da8f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt @@ -3,8 +3,8 @@ package org.koitharu.kotatsu.history.ui import android.content.Context import androidx.lifecycle.LifecycleOwner import coil.ImageLoader -import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListListener diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 0ad1f5e92..47157c4a7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -3,15 +3,16 @@ package org.koitharu.kotatsu.history.ui import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View import androidx.appcompat.view.ActionMode import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.addMenuProvider @AndroidEntryPoint class HistoryListFragment : MangaListFragment() { @@ -19,9 +20,9 @@ class HistoryListFragment : MangaListFragment() { override val viewModel by viewModels() override val isSwipeRefreshEnabled = false - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - addMenuProvider(HistoryListMenuProvider(view.context, viewModel)) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel)) viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } @@ -48,6 +49,7 @@ class HistoryListFragment : MangaListFragment() { mode.finish() true } + else -> super.onActionItemClicked(controller, mode, item) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index a3d5663ff..673fbc87d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -1,24 +1,27 @@ package org.koitharu.kotatsu.history.ui -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.util.ReversibleAction -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.daysDiff +import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.MangaWithHistory -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.history.domain.model.MangaWithHistory +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -27,11 +30,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListModel -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.daysDiff -import org.koitharu.kotatsu.utils.ext.emitValue -import org.koitharu.kotatsu.utils.ext.onFirst import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -40,20 +38,20 @@ import javax.inject.Inject class HistoryListViewModel @Inject constructor( private val repository: HistoryRepository, private val settings: AppSettings, - private val trackingRepository: TrackingRepository, - private val tagHighlighter: MangaTagHighlighter, + private val extraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { - val isGroupingEnabled = MutableLiveData() - - private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled } - .onEach { isGroupingEnabled.emitValue(it) } + val isGroupingEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, + key = AppSettings.KEY_HISTORY_GROUPING, + valueProducer = { isHistoryGroupingEnabled }, + ) override val content = combine( repository.observeAllWithHistory(), - historyGrouping, - listModeFlow, + isGroupingEnabled, + listMode, ) { list, grouped, mode -> when { list.isEmpty() -> listOf( @@ -73,7 +71,7 @@ class HistoryListViewModel @Inject constructor( loadingCounter.decrement() }.catch { emit(listOf(it.toErrorState(canRetry = false))) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) override fun onRefresh() = Unit @@ -91,7 +89,7 @@ class HistoryListViewModel @Inject constructor( } launchJob(Dispatchers.Default) { val handle = repository.delete(ids) - onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle)) + onActionDone.call(ReversibleAction(R.string.removed_from_history, handle)) } } @@ -105,7 +103,6 @@ class HistoryListViewModel @Inject constructor( mode: ListMode, ): List { val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1) - val showPercent = settings.isReadingIndicatorsEnabled var prevDate: DateTimeAgo? = null for ((manga, history) in list) { if (grouped) { @@ -115,16 +112,10 @@ class HistoryListViewModel @Inject constructor( } prevDate = date } - val counter = if (settings.isTrackerEnabled) { - trackingRepository.getNewChaptersCount(manga.id) - } else { - 0 - } - val percent = if (showPercent) history.percent else PROGRESS_NONE result += when (mode) { - ListMode.LIST -> manga.toListModel(counter, percent) - ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent, tagHighlighter) - ListMode.GRID -> manga.toGridModel(counter, percent) + ListMode.LIST -> manga.toListModel(extraProvider) + ListMode.DETAILED_LIST -> manga.toListDetailedModel(extraProvider) + ListMode.GRID -> manga.toGridModel(extraProvider) } } return result diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt index dc4d1e22b..618b8d788 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt @@ -1,14 +1,19 @@ package org.koitharu.kotatsu.history.ui.util import android.content.Context -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.annotation.StyleRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.ColorUtils import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import org.koitharu.kotatsu.utils.ext.scale +import org.koitharu.kotatsu.core.util.ext.scale +import org.koitharu.kotatsu.history.data.PROGRESS_NONE class ReadingProgressDrawable( context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt index 243e8cb5d..744201215 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt @@ -11,8 +11,8 @@ import android.view.animation.AccelerateDecelerateInterpolator import androidx.annotation.AttrRes import androidx.annotation.StyleRes import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import org.koitharu.kotatsu.utils.ext.getAnimationDuration +import org.koitharu.kotatsu.core.util.ext.getAnimationDuration +import org.koitharu.kotatsu.history.data.PROGRESS_NONE class ReadingProgressView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt index 9794039e8..469ea71c7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/ImageActivity.kt @@ -17,12 +17,12 @@ import coil.target.ViewTarget import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.indicator import org.koitharu.kotatsu.databinding.ActivityImageBinding import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat -import org.koitharu.kotatsu.utils.ext.indicator import javax.inject.Inject @AndroidEntryPoint @@ -42,7 +42,7 @@ class ImageActivity : BaseActivity() { } override fun onWindowInsetsChanged(insets: Insets) { - with(binding.toolbar) { + with(viewBinding.toolbar) { updatePadding( left = insets.left, right = insets.right, @@ -59,8 +59,8 @@ class ImageActivity : BaseActivity() { .memoryCachePolicy(CachePolicy.DISABLED) .lifecycle(this) .tag(intent.getSerializableExtraCompat(EXTRA_SOURCE)) - .target(SsivTarget(binding.ssiv)) - .indicator(binding.progressBar) + .target(SsivTarget(viewBinding.ssiv)) + .indicator(viewBinding.progressBar) .enqueueWith(coil) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt new file mode 100644 index 000000000..60cc18885 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt @@ -0,0 +1,60 @@ +package org.koitharu.kotatsu.list.domain + +import android.content.Context +import androidx.annotation.ColorRes +import dagger.Reusable +import dagger.hilt.android.qualifiers.ApplicationContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.history.data.PROGRESS_NONE +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import javax.inject.Inject + +@Reusable +class ListExtraProvider @Inject constructor( + @ApplicationContext context: Context, + private val settings: AppSettings, + private val trackingRepository: TrackingRepository, + private val historyRepository: HistoryRepository, +) { + + private val dict by lazy { + context.resources.openRawResource(R.raw.tags_redlist).use { + val set = HashSet() + it.bufferedReader().forEachLine { x -> + val line = x.trim() + if (line.isNotEmpty()) { + set.add(line) + } + } + set + } + } + + suspend fun getCounter(mangaId: Long): Int { + return if (settings.isTrackerEnabled) { + trackingRepository.getNewChaptersCount(mangaId) + } else { + 0 + } + } + + suspend fun getProgress(mangaId: Long): Float { + return if (settings.isReadingIndicatorsEnabled) { + historyRepository.getProgress(mangaId) + } else { + PROGRESS_NONE + } + } + + @ColorRes + fun getTagTint(tag: MangaTag): Int { + return if (tag.title.lowercase() in dict) { + R.color.warning + } else { + 0 + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ItemSizeResolver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt index 37339aac2..75b18132d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/ListModeBottomSheet.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.list.ui 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 @@ -10,30 +9,30 @@ import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.slider.Slider import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ext.setValueRounded +import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.databinding.DialogListModeBinding -import org.koitharu.kotatsu.utils.ext.setValueRounded -import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter import javax.inject.Inject @AndroidEntryPoint class ListModeBottomSheet : - BaseBottomSheet(), + BaseAdaptiveSheet(), Slider.OnChangeListener, MaterialButtonToggleGroup.OnButtonCheckedListener { @Inject lateinit var settings: AppSettings - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = DialogListModeBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogListModeBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val mode = settings.listMode binding.buttonList.isChecked = mode == ListMode.LIST binding.buttonListDetailed.isChecked = mode == ListMode.DETAILED_LIST @@ -41,7 +40,7 @@ class ListModeBottomSheet : binding.textViewGridTitle.isVisible = mode == ListMode.GRID binding.sliderGrid.isVisible = mode == ListMode.GRID - binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter(view.context)) + binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter(binding.root.context)) binding.sliderGrid.setValueRounded(settings.gridSize.toFloat()) binding.sliderGrid.addOnChangeListener(this) @@ -58,8 +57,8 @@ class ListModeBottomSheet : R.id.button_grid -> ListMode.GRID else -> return } - binding.textViewGridTitle.isVisible = mode == ListMode.GRID - binding.sliderGrid.isVisible = mode == ListMode.GRID + requireViewBinding().textViewGridTitle.isVisible = mode == ListMode.GRID + requireViewBinding().sliderGrid.isVisible = mode == ListMode.GRID settings.listMode = mode } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 2aad241b3..021f4d83a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -20,22 +20,31 @@ import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager -import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager -import org.koitharu.kotatsu.base.ui.list.ListSelectionController -import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration -import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration -import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller -import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager +import org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.clearItemDecorations +import org.koitharu.kotatsu.core.util.ext.measureHeight +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -46,16 +55,8 @@ import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.clearItemDecorations -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.measureHeight -import org.koitharu.kotatsu.utils.ext.resolveDp -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import javax.inject.Inject @AndroidEntryPoint @@ -88,18 +89,18 @@ abstract class MangaListFragment : protected val selectedItems: Set get() = collectSelectedItems() - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentListBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) listAdapter = onCreateAdapter() - spanResolver = MangaListSpanResolver(view.resources) + spanResolver = MangaListSpanResolver(binding.root.resources) selectionController = ListSelectionController( activity = requireActivity(), - decoration = MangaSelectionDecoration(view.context), + decoration = MangaSelectionDecoration(binding.root.context), registryOwner = this, callback = this, ) @@ -112,8 +113,6 @@ abstract class MangaListFragment : fastScroller.setFastScrollListener(this@MangaListFragment) } with(binding.swipeRefreshLayout) { - setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary)) - setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)) setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } @@ -123,9 +122,9 @@ abstract class MangaListFragment : viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged) - viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) - viewModel.onDownloadStarted.observe(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) + viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) } override fun onDestroyView() { @@ -149,8 +148,8 @@ abstract class MangaListFragment : override fun onReadClick(manga: Manga, view: View) { if (selectionController?.onItemClick(manga.id) != true) { - val intent = ReaderActivity.newIntent(context ?: return, manga) - startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) + val intent = IntentBuilder(view.context).manga(manga).build() + startActivity(intent, scaleUpActivityOptionsOf(view)) } } @@ -163,7 +162,7 @@ abstract class MangaListFragment : @CallSuper override fun onRefresh() { - binding.swipeRefreshLayout.isRefreshing = true + requireViewBinding().swipeRefreshLayout.isRefreshing = true viewModel.onRefresh() } @@ -185,10 +184,10 @@ abstract class MangaListFragment : @CallSuper protected open fun onLoadingStateChanged(isLoading: Boolean) { - binding.swipeRefreshLayout.isEnabled = binding.swipeRefreshLayout.isRefreshing || + requireViewBinding().swipeRefreshLayout.isEnabled = requireViewBinding().swipeRefreshLayout.isRefreshing || isSwipeRefreshEnabled && !isLoading if (!isLoading) { - binding.swipeRefreshLayout.isRefreshing = false + requireViewBinding().swipeRefreshLayout.isRefreshing = false } } @@ -201,15 +200,15 @@ abstract class MangaListFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) - binding.recyclerView.fastScroller.updateLayoutParams { + requireViewBinding().recyclerView.fastScroller.updateLayoutParams { bottomMargin = insets.bottom } if (activity is MainActivity) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top - binding.swipeRefreshLayout.setProgressViewOffset( + requireViewBinding().swipeRefreshLayout.setProgressViewOffset( true, headerHeight + resources.resolveDp(-72), headerHeight + resources.resolveDp(10), @@ -233,12 +232,12 @@ abstract class MangaListFragment : private fun onGridScaleChanged(scale: Float) { spanSizeLookup.invalidateCache() - spanResolver?.setGridSize(scale, binding.recyclerView) + spanResolver?.setGridSize(scale, requireViewBinding().recyclerView) } private fun onListModeChanged(mode: ListMode) { spanSizeLookup.invalidateCache() - with(binding.recyclerView) { + with(requireViewBinding().recyclerView) { clearItemDecorations() removeOnLayoutChangeListener(spanResolver) when (mode) { @@ -269,7 +268,7 @@ abstract class MangaListFragment : addOnLayoutChangeListener(spanResolver) } } - selectionController?.attachToRecyclerView(binding.recyclerView) + selectionController?.attachToRecyclerView(requireViewBinding().recyclerView) } } @@ -294,7 +293,7 @@ abstract class MangaListFragment : } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems) + FavouriteCategoriesSheet.show(childFragmentManager, selectedItems) mode.finish() true } @@ -310,16 +309,16 @@ abstract class MangaListFragment : } override fun onSelectionChanged(controller: ListSelectionController, count: Int) { - binding.recyclerView.invalidateItemDecorations() + requireViewBinding().recyclerView.invalidateItemDecorations() } override fun onFastScrollStart(fastScroller: FastScroller) { (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) - binding.swipeRefreshLayout.isEnabled = false + requireViewBinding().swipeRefreshLayout.isEnabled = false } override fun onFastScrollStop(fastScroller: FastScroller) { - binding.swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled + requireViewBinding().swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled } private fun collectSelectedItems(): Set { @@ -343,7 +342,7 @@ abstract class MangaListFragment : override fun getSpanSize(position: Int): Int { val total = - (binding.recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 + (requireViewBinding().recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 return when (listAdapter?.getItemViewType(position)) { ITEM_TYPE_MANGA_GRID -> 1 else -> total diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListSpanResolver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt similarity index 55% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 6ff381fe4..8c197fae9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -1,39 +1,38 @@ package org.koitharu.kotatsu.list.ui -import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.prefs.observeAsLiveData +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData abstract class MangaListViewModel( - private val settings: AppSettings, + settings: AppSettings, private val downloadScheduler: DownloadWorker.Scheduler, ) : BaseViewModel() { - abstract val content: LiveData> - protected val listModeFlow = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode } - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, settings.listMode) - val listMode = listModeFlow.asFlowLiveData(viewModelScope.coroutineContext) - val onActionDone = SingleLiveEvent() - val gridScale = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + abstract val content: StateFlow> + val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode } + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.listMode) + val onActionDone = MutableEventFlow() + val gridScale = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_GRID_SIZE, valueProducer = { gridSize / 100f }, ) - val onDownloadStarted = SingleLiveEvent() + val onDownloadStarted = MutableEventFlow() open fun onUpdateFilter(tags: Set) = Unit @@ -44,7 +43,7 @@ abstract class MangaListViewModel( fun download(items: Set) { launchJob(Dispatchers.Default) { downloadScheduler.schedule(items) - onDownloadStarted.emitCall(Unit) + onDownloadStarted.call(Unit) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt index b8d5e03c9..5518e15a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaSelectionDecoration.kt @@ -12,10 +12,10 @@ import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.util.ext.getItem +import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.list.ui.model.MangaItemModel -import org.koitharu.kotatsu.utils.ext.getItem -import org.koitharu.kotatsu.utils.ext.getThemeColor import com.google.android.material.R as materialR open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt index d03a7cdff..72bef49ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyHintAD.kt @@ -3,13 +3,13 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible 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.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun emptyHintAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt index 29a4b1dbb..745eb583e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/EmptyStateListAD.kt @@ -3,13 +3,13 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun emptyStateListAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt index 52b3db95a..4e25a5eb7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorFooterAD.kt @@ -1,15 +1,15 @@ package org.koitharu.kotatsu.list.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.databinding.ItemErrorFooterBinding import org.koitharu.kotatsu.list.ui.model.ErrorFooter import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.getDisplayMessage fun errorFooterAD( listener: MangaListListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }, ) { binding.root.setOnClickListener { @@ -20,4 +20,4 @@ fun errorFooterAD( binding.textViewTitle.text = item.exception.getDisplayMessage(context.resources) binding.imageViewIcon.setImageResource(item.icon) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt index a31b55d3f..03de52eb8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ErrorStateListAD.kt @@ -2,15 +2,15 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.core.view.isVisible import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.databinding.ItemErrorStateBinding import org.koitharu.kotatsu.list.ui.model.ErrorState import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.getDisplayMessage fun errorStateListAD( listener: ListStateHolderListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }, ) { binding.buttonRetry.setOnClickListener { @@ -27,4 +27,4 @@ fun errorStateListAD( setText(item.buttonText) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt similarity index 57% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt index d8ab7ce94..518983bd4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeader2AD.kt @@ -1,24 +1,20 @@ package org.koitharu.kotatsu.list.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.core.ui.titleRes -import org.koitharu.kotatsu.databinding.ItemHeader2Binding -import org.koitharu.kotatsu.list.ui.model.ListHeader2 +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding +import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled -import org.koitharu.kotatsu.utils.ext.setTextAndVisible +@Deprecated("") fun listHeader2AD( listener: MangaListListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) }, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> FragmentFilterHeaderBinding.inflate(layoutInflater, parent, false) }, ) { var ignoreChecking = false - binding.textViewFilter.setOnClickListener { - listener.onFilterClick(it) - } binding.chipsTags.setOnCheckedStateChangeListener { _, _ -> if (!ignoreChecking) { listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java)) @@ -36,6 +32,5 @@ fun listHeader2AD( ignoreChecking = true binding.chipsTags.setChips(item.chips) // TODO use recyclerview ignoreChecking = false - binding.textViewFilter.setTextAndVisible(item.sortOrder?.titleRes ?: 0) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt similarity index 53% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index 855c70c1f..8076dc68b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -1,18 +1,21 @@ package org.koitharu.kotatsu.list.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding +import org.koitharu.kotatsu.databinding.ItemHeaderSingleBinding import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun listHeaderAD( - listener: ListHeaderClickListener, + listener: ListHeaderClickListener?, ) = adapterDelegateViewBinding( { inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) }, ) { - binding.buttonMore.setOnClickListener { - listener.onListHeaderClick(item, it) + if (listener != null) { + binding.buttonMore.setOnClickListener { + listener.onListHeaderClick(item, it) + } } bind { @@ -20,3 +23,12 @@ fun listHeaderAD( binding.buttonMore.setTextAndVisible(item.buttonTextRes) } } + +fun listSimpleHeaderAD() = adapterDelegateViewBinding( + { inflater, parent -> ItemHeaderSingleBinding.inflate(inflater, parent, false) }, +) { + + bind { + binding.textViewTitle.text = item.getText(context) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderClickListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderClickListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderClickListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListHeaderClickListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListStateHolderListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/LoadingFooterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/LoadingFooterAD.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/LoadingFooterAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/LoadingFooterAD.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/LoadingStateAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/LoadingStateAD.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/LoadingStateAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/LoadingStateAD.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt index 9bb885da4..d1cf4f20c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaDetailsClickListener.kt @@ -1,7 +1,7 @@ package org.koitharu.kotatsu.list.ui.adapter import android.view.View -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index 98859c239..e47650a25 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -5,18 +5,18 @@ import coil.ImageLoader import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemMangaGridBinding -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun mangaGridItemAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 1be69ef00..09a4809d6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -4,10 +4,11 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo +import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.list.ui.model.ListHeader -import org.koitharu.kotatsu.list.ui.model.ListHeader2 import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel @@ -60,6 +61,10 @@ open class MangaListAdapter( oldItem.dateTimeAgo == newItem.dateTimeAgo } + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + else -> oldItem.javaClass == newItem.javaClass } @@ -77,7 +82,7 @@ open class MangaListAdapter( } } - is ListHeader2 -> Unit + is FilterHeaderModel -> Unit else -> super.getChangePayload(oldItem, newItem) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 26ced553f..95396fca3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -8,18 +8,18 @@ import com.google.android.material.badge.BadgeDrawable import com.google.android.material.chip.Chip import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.ext.textAndVisible -import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun mangaListDetailedItemAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 48904c2c4..77f68418d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -5,16 +5,16 @@ import coil.ImageLoader import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.ext.textAndVisible fun mangaListItemAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt index 98a4bc8f6..3a615aab5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/RelatedDateItemAD.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter import android.widget.TextView import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.ListModel fun relatedDateItemAD() = adapterDelegate(R.layout.item_header) { @@ -11,4 +11,4 @@ fun relatedDateItemAD() = adapterDelegate(R.layout.item_ bind { (itemView as TextView).text = item.format(context.resources) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/EmptyHint.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/EmptyState.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/EmptyState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/EmptyState.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ErrorState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorState.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ErrorState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorState.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index 5d9682d75..1be8b5b74 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.list.ui.model import android.content.Context import androidx.annotation.StringRes -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo class ListHeader private constructor( val text: CharSequence?, @@ -46,9 +46,7 @@ class ListHeader private constructor( if (textRes != other.textRes) return false if (dateTimeAgo != other.dateTimeAgo) return false if (buttonTextRes != other.buttonTextRes) return false - if (payload != other.payload) return false - - return true + return payload == other.payload } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt similarity index 54% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index 20fba3c37..aa6014626 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -1,48 +1,45 @@ package org.koitharu.kotatsu.list.ui.model import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.ext.ifZero +import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.ifZero import java.net.SocketTimeoutException import java.net.UnknownHostException -fun Manga.toListModel( - counter: Int, - progress: Float, +suspend fun Manga.toListModel( + extraProvider: ListExtraProvider? ) = MangaListModel( id = id, title = title, subtitle = tags.joinToString(", ") { it.title }, coverUrl = coverUrl, manga = this, - counter = counter, - progress = progress, + counter = extraProvider?.getCounter(id) ?: 0, + progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE, ) -fun Manga.toListDetailedModel( - counter: Int, - progress: Float, - tagHighlighter: MangaTagHighlighter?, +suspend fun Manga.toListDetailedModel( + extraProvider: ListExtraProvider?, ) = MangaListDetailedModel( id = id, title = title, subtitle = altTitle, coverUrl = coverUrl, manga = this, - counter = counter, - progress = progress, + counter = extraProvider?.getCounter(id) ?: 0, + progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE, tags = tags.map { ChipsView.ChipModel( - tint = tagHighlighter?.getTint(it) ?: 0, + tint = extraProvider?.getTagTint(it) ?: 0, title = it.title, + icon = 0, isCheckable = false, isChecked = false, data = it, @@ -50,53 +47,30 @@ fun Manga.toListDetailedModel( }, ) -fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel( +suspend fun Manga.toGridModel( + extraProvider: ListExtraProvider?, +) = MangaGridModel( id = id, title = title, coverUrl = coverUrl, manga = this, - counter = counter, - progress = progress, + counter = extraProvider?.getCounter(id) ?: 0, + progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE, ) suspend fun List.toUi( mode: ListMode, extraProvider: ListExtraProvider, - tagHighlighter: MangaTagHighlighter?, -): List = toUi(ArrayList(size), mode, extraProvider, tagHighlighter) - -fun List.toUi( - mode: ListMode, - tagHighlighter: MangaTagHighlighter?, -): List = toUi(ArrayList(size), mode, tagHighlighter) - -fun > List.toUi( - destination: C, - mode: ListMode, - tagHighlighter: MangaTagHighlighter?, -): C = when (mode) { - ListMode.LIST -> mapTo(destination) { it.toListModel(0, PROGRESS_NONE) } - ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0, PROGRESS_NONE, tagHighlighter) } - ListMode.GRID -> mapTo(destination) { it.toGridModel(0, PROGRESS_NONE) } -} +): List = toUi(ArrayList(size), mode, extraProvider) suspend fun > List.toUi( destination: C, mode: ListMode, extraProvider: ListExtraProvider, - tagHighlighter: MangaTagHighlighter?, ): C = when (mode) { - ListMode.LIST -> mapTo(destination) { - it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) - } - - ListMode.DETAILED_LIST -> mapTo(destination) { - it.toListDetailedModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id), tagHighlighter) - } - - ListMode.GRID -> mapTo(destination) { - it.toGridModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) - } + ListMode.LIST -> mapTo(destination) { it.toListModel(extraProvider) } + ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(extraProvider) } + ListMode.GRID -> mapTo(destination) { it.toGridModel(extraProvider) } } fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt new file mode 100644 index 000000000..c7c336a7a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/LoadingFooter.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.list.ui.model + +class LoadingFooter @JvmOverloads constructor( + val key: Int = 0, +) : ListModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LoadingFooter + + return key == other.key + } + + override fun hashCode(): Int { + return key + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/LoadingState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/LoadingState.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/LoadingState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/LoadingState.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt index d1f40dc46..1d246ebb2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.list.ui.model -import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.parsers.model.Manga data class MangaListDetailedModel( diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CacheDir.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CacheDir.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/CacheDir.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/CacheDir.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFetcher.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/local/data/CbzFilter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt index f74d30258..24e9f8a2d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.local.data +import android.net.Uri import java.io.File import java.io.FileFilter import java.io.FilenameFilter @@ -21,5 +22,10 @@ class CbzFilter : FileFilter, FilenameFilter { val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) return ext == "cbz" || ext == "zip" } + + fun isUriSupported(uri: Uri): Boolean { + val scheme = uri.scheme?.lowercase(Locale.ROOT) + return scheme != null && scheme == "cbz" || scheme == "zip" + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/ImageFileFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/ImageFileFilter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 2ca31d0e5..9405629cf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.local.domain +package org.koitharu.kotatsu.local.data import android.net.Uri import androidx.core.net.toFile @@ -11,26 +11,25 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.local.data.LocalManga -import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.data.LocalStorageManager -import org.koitharu.kotatsu.local.data.TempFileFilter +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.CompositeMutex +import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.data.output.LocalMangaUtil +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.AlphanumComparator -import org.koitharu.kotatsu.utils.CompositeMutex -import org.koitharu.kotatsu.utils.ext.deleteAwait -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File +import java.util.EnumSet import javax.inject.Inject import javax.inject.Singleton @@ -40,11 +39,20 @@ private const val MAX_PARALLELISM = 4 class LocalMangaRepository @Inject constructor( private val storageManager: LocalStorageManager, @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, + private val settings: AppSettings, ) : MangaRepository { override val source = MangaSource.LOCAL private val locks = CompositeMutex() + override val sortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST) + + override var defaultSortOrder: SortOrder + get() = settings.localListOrder + set(value) { + settings.localListOrder = value + } + override suspend fun getList(offset: Int, query: String): List { if (offset > 0) { return emptyList() @@ -65,7 +73,7 @@ class LocalMangaRepository @Inject constructor( list.retainAll { x -> x.containsTags(tags) } } when (sortOrder) { - SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title }) + SortOrder.ALPHABETICAL -> list.sortWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.manga.title }) SortOrder.RATING -> list.sortByDescending { it.manga.rating } SortOrder.NEWEST, SortOrder.UPDATED, @@ -100,8 +108,11 @@ class LocalMangaRepository @Inject constructor( suspend fun deleteChapters(manga: Manga, ids: Set) { lockManga(manga.id) try { - LocalMangaUtil(manga).deleteChapters(ids) - localStorageChanges.emit(LocalManga(manga)) + val subject = if (manga.isLocal) manga else checkNotNull(findSavedManga(manga)) { + "Manga is not stored on local storage" + }.manga + LocalMangaUtil(subject).deleteChapters(ids) + localStorageChanges.emit(LocalManga(subject)) } finally { unlockManga(manga.id) } @@ -137,8 +148,6 @@ class LocalMangaRepository @Inject constructor( }.firstOrNull()?.getManga() } - override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING) - override suspend fun getPageUrl(page: MangaPage) = page.url override suspend fun getTags() = emptySet() diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt index 7653dbf07..9684dd63c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import okhttp3.Cache import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.computeSize +import org.koitharu.kotatsu.core.util.ext.getStorageName import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.utils.ext.computeSize -import org.koitharu.kotatsu.utils.ext.getStorageName import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt index eb216a140..6d08885af 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt @@ -14,7 +14,6 @@ import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.getStringOrNull import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet import org.koitharu.kotatsu.parsers.util.toTitleCase -import org.koitharu.kotatsu.utils.AlphanumComparator import java.io.File class MangaIndex(source: String?) { @@ -126,7 +125,7 @@ class MangaIndex(source: String?) { item.put("id", id) list.add(item) } - val comparator = AlphanumComparator() + val comparator = org.koitharu.kotatsu.core.util.AlphanumComparator() list.sortWith(compareBy(comparator) { it.getString("name") }) val newJo = JSONObject() list.forEachIndexed { i, obj -> diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt similarity index 60% rename from app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt index 62c625042..ad52d2d26 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.local.data import android.content.Context +import android.os.StatFs import com.tomclaw.cache.DiskLruCache import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -9,15 +10,15 @@ import kotlinx.coroutines.withContext import okio.Source import okio.buffer import okio.sink +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.subdir +import org.koitharu.kotatsu.core.util.ext.takeIfReadable +import org.koitharu.kotatsu.core.util.ext.takeIfWriteable +import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.parsers.util.SuspendLazy -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.longHashCode -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.subdir -import org.koitharu.kotatsu.utils.ext.takeIfReadable -import org.koitharu.kotatsu.utils.ext.takeIfWriteable -import org.koitharu.kotatsu.utils.ext.writeAllCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -33,7 +34,8 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { } private val lruCache = SuspendLazy { val dir = cacheDir.get() - val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES) + val availableSize = (getAvailableSize() * 0.8).toLong() + val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN) runCatchingCancellable { DiskLruCache.create(dir, size) }.recoverCatching { error -> @@ -63,4 +65,20 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { file.delete() } } + + private suspend fun getAvailableSize(): Long = runCatchingCancellable { + val statFs = StatFs(cacheDir.get().absolutePath) + statFs.availableBytes + }.onFailure { + it.printStackTraceDebug() + }.getOrDefault(SIZE_DEFAULT) + + private companion object { + + val SIZE_MIN + get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES) + + val SIZE_DEFAULT + get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES) + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/TrackerLogger.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/Qualifiers.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/TrackerLogger.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/Qualifiers.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/TempFileFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/TempFileFilter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt index 70307defa..9221a4f08 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt @@ -13,13 +13,13 @@ import okio.buffer import okio.sink import okio.source import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException +import org.koitharu.kotatsu.core.util.ext.resolveName +import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.local.data.CbzFilter -import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.input.LocalMangaInput -import org.koitharu.kotatsu.utils.ext.resolveName -import org.koitharu.kotatsu.utils.ext.writeAllCancellable +import org.koitharu.kotatsu.local.domain.model.LocalManga import java.io.File import java.io.IOException import javax.inject.Inject diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt index 9dc597eb0..c2cac2c60 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt @@ -4,20 +4,19 @@ import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.util.ext.listFilesRecursive +import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.ImageFileFilter -import org.koitharu.kotatsu.local.data.LocalManga import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.output.LocalMangaOutput +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toCamelCase -import org.koitharu.kotatsu.utils.AlphanumComparator -import org.koitharu.kotatsu.utils.ext.listFilesRecursive -import org.koitharu.kotatsu.utils.ext.longHashCode -import org.koitharu.kotatsu.utils.ext.toListSorted import java.io.File import java.util.zip.ZipFile @@ -89,7 +88,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { file.listFilesRecursive(ImageFileFilter()) - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) .map { val pageUri = it.toUri().toString() MangaPage( @@ -105,7 +104,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { .asSequence() .filter { x -> !x.isDirectory } .map { it.name } - .toListSorted(AlphanumComparator()) + .toListSorted(org.koitharu.kotatsu.core.util.AlphanumComparator()) .map { val pageUri = zipUri(file, it) MangaPage( @@ -122,7 +121,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun String.toHumanReadable() = replace("_", " ").toCamelCase() private fun getChaptersFiles(): List = root.listFilesRecursive(CbzFilter()) - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) private fun findFirstImageEntry(): String? { val filter = ImageFileFilter() @@ -131,7 +130,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { } val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null return ZipFile(cbz).use { zip -> - val filter = ImageFileFilter() zip.entries().asSequence() .firstOrNull { x -> !x.isDirectory && filter.accept(x) } ?.let { entry -> zipUri(cbz, entry.name) } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt index 0da957b8b..f203912e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.local.data.input import android.net.Uri import androidx.core.net.toFile -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt index 7c01e50b8..f468c4647 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt @@ -7,18 +7,17 @@ import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.readText +import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.output.LocalMangaOutput +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toCamelCase -import org.koitharu.kotatsu.utils.AlphanumComparator -import org.koitharu.kotatsu.utils.ext.longHashCode -import org.koitharu.kotatsu.utils.ext.readText -import org.koitharu.kotatsu.utils.ext.toListSorted import java.io.File import java.util.Enumeration import java.util.zip.ZipEntry @@ -71,18 +70,19 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { publicUrl = fileUri, source = MangaSource.LOCAL, coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()), - chapters = chapters.sortedWith(AlphanumComparator()).mapIndexed { i, s -> - MangaChapter( - id = "$i$s".longHashCode(), - name = s.ifEmpty { title }, - number = i + 1, - source = MangaSource.LOCAL, - uploadDate = 0L, - url = uriBuilder.fragment(s).build().toString(), - scanlator = null, - branch = null, - ) - }, + chapters = chapters.sortedWith(org.koitharu.kotatsu.core.util.AlphanumComparator()) + .mapIndexed { i, s -> + MangaChapter( + id = "$i$s".longHashCode(), + name = s.ifEmpty { title }, + number = i + 1, + source = MangaSource.LOCAL, + uploadDate = 0L, + url = uriBuilder.fragment(s).build().toString(), + scanlator = null, + branch = null, + ) + }, altTitle = null, rating = -1f, isNsfw = false, @@ -125,7 +125,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { } } entries - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) .map { x -> val entryUri = zipUri(file, x.name) MangaPage( @@ -141,7 +141,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { private fun findFirstImageEntry(entries: Enumeration): ZipEntry? { val list = entries.toList() .filterNot { it.isDirectory } - .sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) + .sortedWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) val map = MimeTypeMap.getSingleton() return list.firstOrNull { map.getMimeTypeFromExtension(it.name.substringAfterLast('.')) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index 4e97b188e..ef050f1cc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -2,13 +2,13 @@ package org.koitharu.kotatsu.local.data.output import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.util.ext.deleteAwait +import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.toFileNameSafe -import org.koitharu.kotatsu.utils.ext.deleteAwait -import org.koitharu.kotatsu.utils.ext.takeIfReadable import java.io.File class LocalMangaDirOutput( diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaUtil.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt index 6c1fda62e..23caca4fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaZipOutput.kt @@ -3,12 +3,12 @@ package org.koitharu.kotatsu.local.data.output import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.util.ext.deleteAwait +import org.koitharu.kotatsu.core.util.ext.readText import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.utils.ext.deleteAwait -import org.koitharu.kotatsu.utils.ext.readText import java.io.File import java.util.zip.ZipFile diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/util/ExtraCloseableSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/util/ExtraCloseableSource.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/data/util/ExtraCloseableSource.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/data/util/ExtraCloseableSource.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt new file mode 100644 index 000000000..07a22f924 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.local.domain + +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import java.io.IOException +import javax.inject.Inject + +class DeleteLocalMangaUseCase @Inject constructor( + private val localMangaRepository: LocalMangaRepository, + private val historyRepository: HistoryRepository, +) { + + suspend operator fun invoke(manga: Manga) { + val victim = if (manga.isLocal) manga else localMangaRepository.findSavedManga(manga)?.manga + checkNotNull(victim) { "Cannot find saved manga for ${manga.title}" } + val original = if (manga.isLocal) localMangaRepository.getRemoteManga(manga) else manga + localMangaRepository.delete(victim) || throw IOException("Unable to delete file") + runCatchingCancellable { + historyRepository.deleteOrSwap(victim, original) + }.onFailure { + it.printStackTraceDebug() + } + } + + suspend operator fun invoke(ids: Set) { + val list = localMangaRepository.getList(0, null, null) + var removed = 0 + for (manga in list) { + if (manga.id in ids) { + invoke(manga) + removed++ + } + } + check(removed == ids.size) { + "Removed $removed files but ${ids.size} requested" + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/local/data/LocalManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt index bebb4c12c..247d395d4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalManga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/model/LocalManga.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.local.data +package org.koitharu.kotatsu.local.domain.model import androidx.core.net.toFile import androidx.core.net.toUri @@ -38,9 +38,7 @@ class LocalManga( other as LocalManga if (manga != other.manga) return false - if (file != other.file) return false - - return true + return file == other.file } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt index b72f82200..7436ccc65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt @@ -10,9 +10,8 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.databinding.DialogImportBinding -import org.koitharu.kotatsu.settings.backup.BackupDialogFragment import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment class ImportDialogFragment : AlertDialogFragment(), View.OnClickListener { @@ -27,7 +26,7 @@ class ImportDialogFragment : AlertDialogFragment(), View.On restoreBackup(it) } - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogImportBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogImportBinding { return DialogImportBinding.inflate(inflater, container, false) } @@ -38,8 +37,8 @@ class ImportDialogFragment : AlertDialogFragment(), View.On .setCancelable(true) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogImportBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.buttonDir.setOnClickListener(this) binding.buttonFile.setOnClickListener(this) binding.buttonBackup.setOnClickListener(this) @@ -64,9 +63,10 @@ class ImportDialogFragment : AlertDialogFragment(), View.On } private fun restoreBackup(uri: Uri?) { - RestoreDialogFragment.newInstance(uri ?: return) - .show(parentFragmentManager, BackupDialogFragment.TAG) - dismiss() + if (uri != null) { + RestoreDialogFragment.show(parentFragmentManager, uri) + dismiss() + } } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/ImportWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt index dc2af369a..fc719ae5b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt @@ -23,13 +23,13 @@ import coil.request.ImageRequest import dagger.assisted.Assisted import dagger.assisted.AssistedInject import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull +import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.toBitmapOrNull -import org.koitharu.kotatsu.utils.ext.toUriOrNull @HiltWorker class ImportWorker @AssistedInject constructor( diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt index e7606dfcf..754c8ffbb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt @@ -11,14 +11,14 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.MutableSharedFlow import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.CoroutineIntentService import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.core.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index 4d88b6663..1cf6c2a79 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -5,27 +5,31 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.appcompat.view.ActionMode -import androidx.appcompat.widget.PopupMenu import androidx.core.net.toFile import androidx.core.net.toUri import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.FragmentListBinding +import org.koitharu.kotatsu.filter.ui.FilterSheetFragment import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.addMenuProvider +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment -class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener { +class LocalListFragment : MangaListFragment() { override val viewModel by viewModels() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick)) - viewModel.onMangaRemoved.observe(viewLifecycleOwner) { onItemRemoved() } + viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() } } override fun onEmptyActionClick() { @@ -33,11 +37,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener } override fun onFilterClick(view: View?) { - super.onFilterClick(view) - val menu = PopupMenu(requireContext(), view ?: binding.recyclerView) - menu.inflate(R.menu.popup_order) - menu.setOnMenuItemClickListener(this) - menu.show() + FilterSheetFragment.show(childFragmentManager) } override fun onScrolledToEnd() = Unit @@ -53,25 +53,16 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener showDeletionConfirm(selectedItemsIds, mode) true } + R.id.action_share -> { val files = selectedItems.map { it.url.toUri().toFile() } ShareHelper(requireContext()).shareCbz(files) mode.finish() true } - else -> super.onActionItemClicked(controller, mode, item) - } - } - override fun onMenuItemClick(item: MenuItem): Boolean { - val order = when (item.itemId) { - R.id.action_order_new -> SortOrder.NEWEST - R.id.action_order_abs -> SortOrder.ALPHABETICAL - R.id.action_order_rating -> SortOrder.RATING - else -> return false + else -> super.onActionItemClicked(controller, mode, item) } - viewModel.setSortOrder(order) - return true } private fun showDeletionConfirm(ids: Set, mode: ActionMode) { @@ -87,11 +78,13 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener } private fun onItemRemoved() { - Snackbar.make(binding.recyclerView, R.string.removal_completed, Snackbar.LENGTH_SHORT).show() + Snackbar.make(requireViewBinding().recyclerView, R.string.removal_completed, Snackbar.LENGTH_SHORT).show() } companion object { - fun newInstance() = LocalListFragment() + fun newInstance() = LocalListFragment().withArgs(1) { + putSerializable(RemoteListFragment.ARG_SOURCE, MangaSource.LOCAL) // required by FilterCoordinator + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt new file mode 100644 index 000000000..99723036a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.local.ui + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharedFlow +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.filter.ui.FilterCoordinator +import org.koitharu.kotatsu.list.domain.ListExtraProvider +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.local.data.LocalStorageChanges +import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase +import org.koitharu.kotatsu.local.domain.model.LocalManga +import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel +import javax.inject.Inject + +@HiltViewModel +class LocalListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + mangaRepositoryFactory: MangaRepository.Factory, + filter: FilterCoordinator, + settings: AppSettings, + downloadScheduler: DownloadWorker.Scheduler, + listExtraProvider: ListExtraProvider, + private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, + @LocalStorageChanges private val localStorageChanges: SharedFlow, +) : RemoteListViewModel( + savedStateHandle, + mangaRepositoryFactory, + filter, + settings, + listExtraProvider, + downloadScheduler, +) { + + val onMangaRemoved = MutableEventFlow() + + init { + launchJob(Dispatchers.Default) { + localStorageChanges + .collect { + loadList(filter.snapshot(), append = false).join() + } + } + } + + fun delete(ids: Set) { + launchLoadingJob(Dispatchers.Default) { + deleteLocalMangaUseCase(ids) + onMangaRemoved.call(Unit) + } + } + + override fun createEmptyState(canResetFilter: Boolean): EmptyState { + return EmptyState( + icon = R.drawable.ic_empty_local, + textPrimary = R.string.text_local_holder_primary, + textSecondary = R.string.text_local_holder_secondary, + actionStringRes = R.string._import, + ) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt index fc2620b27..995ac48a7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt @@ -11,7 +11,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository import java.util.concurrent.TimeUnit @HiltWorker diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/ExitCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/ExitCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/ExitCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/ExitCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt index 8d66aee1b..0c912e54e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActionButtonBehavior.kt @@ -6,8 +6,8 @@ import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import org.koitharu.kotatsu.base.ui.util.ShrinkOnScrollBehavior -import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView +import org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior +import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView class MainActionButtonBehavior : ShrinkOnScrollBehavior { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index e60500113..395cd5b63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -38,10 +38,19 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.os.VoiceInputContract import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView +import org.koitharu.kotatsu.core.util.ext.drawableEnd +import org.koitharu.kotatsu.core.util.ext.hideKeyboard +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.resolve +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat +import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -51,7 +60,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment @@ -62,13 +71,6 @@ 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.tracker.work.TrackWorker -import org.koitharu.kotatsu.utils.VoiceInputContract -import org.koitharu.kotatsu.utils.ext.drawableEnd -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 javax.inject.Inject import com.google.android.material.R as materialR @@ -94,41 +96,44 @@ class MainActivity : private lateinit var navigationDelegate: MainNavigationDelegate override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override val bottomNav: SlidingBottomNavigationView? - get() = binding.bottomNav + get() = viewBinding.bottomNav override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) if (bottomNav != null) { - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> + ViewCompat.setOnApplyWindowInsetsListener(viewBinding.root) { _, insets -> if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) { val elevation = bottomNav?.elevation ?: 0f window.setNavigationBarTransparentCompat(this@MainActivity, elevation) } insets } - ViewCompat.requestApplyInsets(binding.root) + ViewCompat.requestApplyInsets(viewBinding.root) } - with(binding.searchView) { + with(viewBinding.searchView) { onFocusChangeListener = this@MainActivity searchSuggestionListener = this@MainActivity } window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar) - binding.fab?.setOnClickListener(this) - binding.navRail?.headerView?.setOnClickListener(this) - binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null + viewBinding.fab?.setOnClickListener(this) + viewBinding.navRail?.headerView?.setOnClickListener(this) + viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null - navigationDelegate = MainNavigationDelegate(checkNotNull(bottomNav ?: binding.navRail), supportFragmentManager) + navigationDelegate = MainNavigationDelegate( + navBar = checkNotNull(bottomNav ?: viewBinding.navRail), + fragmentManager = supportFragmentManager, + ) navigationDelegate.addOnFragmentChangedListener(this) - navigationDelegate.onCreate(savedInstanceState) + navigationDelegate.onCreate() - onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container)) + onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container)) onBackPressedDispatcher.addCallback(navigationDelegate) onBackPressedDispatcher.addCallback(closeSearchCallback) @@ -136,8 +141,8 @@ class MainActivity : onFirstStart() } - viewModel.onOpenReader.observe(this, this::onOpenReader) - viewModel.onError.observe(this, SnackbarErrorObserver(binding.container, null)) + viewModel.onOpenReader.observeEvent(this, this::onOpenReader) + viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null)) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged) viewModel.counters.observe(this, ::onCountersChanged) @@ -153,13 +158,15 @@ class MainActivity : override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) { adjustFabVisibility(topFragment = fragment) if (fromUser) { - binding.appbar.setExpanded(true) + actionModeDelegate.finishActionMode() + closeSearchCallback.handleOnBackPressed() + viewBinding.appbar.setExpanded(true) } } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home && !isSearchOpened()) { - binding.searchView.requestFocus() + viewBinding.searchView.requestFocus() return true } return super.onOptionsItemSelected(item) @@ -173,7 +180,7 @@ class MainActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) @@ -198,7 +205,7 @@ class MainActivity : } override fun onQueryClick(query: String, submit: Boolean) { - binding.searchView.query = query + viewBinding.searchView.query = query if (submit) { if (query.isNotEmpty()) { startActivity(MultiSearchActivity.newIntent(this, query)) @@ -216,16 +223,16 @@ class MainActivity : } override fun onVoiceSearchClick() { - val options = binding.searchView.drawableEnd?.bounds?.let { bounds -> + val options = viewBinding.searchView.drawableEnd?.bounds?.let { bounds -> ActivityOptionsCompat.makeScaleUpAnimation( - binding.searchView, + viewBinding.searchView, bounds.centerX(), bounds.centerY(), bounds.width(), bounds.height(), ) } - voiceInputLauncher.tryLaunch(binding.searchView.hint?.toString(), options) + voiceInputLauncher.tryLaunch(viewBinding.searchView.hint?.toString(), options) } override fun onSourceToggle(source: MangaSource, isEnabled: Boolean) { @@ -240,21 +247,21 @@ class MainActivity : override fun onSupportActionModeStarted(mode: ActionMode) { super.onSupportActionModeStarted(mode) adjustFabVisibility() - showNav(false) + bottomNav?.hide() } override fun onSupportActionModeFinished(mode: ActionMode) { super.onSupportActionModeFinished(mode) adjustFabVisibility() - showNav(true) + bottomNav?.show() } private fun onOpenReader(manga: Manga) { - val fab = binding.fab ?: binding.navRail?.headerView + val fab = viewBinding.fab ?: viewBinding.navRail?.headerView val options = fab?.let { - scaleUpActivityOptionsOf(it).toBundle() + scaleUpActivityOptionsOf(it) } - startActivity(ReaderActivity.newIntent(this, manga), options) + startActivity(IntentBuilder(this).manga(manga).build(), options) } private fun onCountersChanged(counters: SparseIntArray) { @@ -270,17 +277,17 @@ class MainActivity : } private fun onIncognitoModeChanged(isIncognito: Boolean) { - var options = binding.searchView.imeOptions + var options = viewBinding.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 + viewBinding.searchView.imeOptions = options } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.fab?.isEnabled = !isLoading + viewBinding.fab?.isEnabled = !isLoading } private fun onResumeEnabledChanged(isEnabled: Boolean) { @@ -293,22 +300,11 @@ class MainActivity : } private fun onSearchClosed() { - binding.searchView.hideKeyboard() + viewBinding.searchView.hideKeyboard() adjustSearchUI(isOpened = false, animate = true) closeSearchCallback.isEnabled = false } - private fun showNav(visible: Boolean) { - bottomNav?.run { - if (visible) { - show() - } else { - hide() - } - } - binding.navRail?.isVisible = visible - } - private fun isSearchOpened(): Boolean { return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null } @@ -337,11 +333,11 @@ class MainActivity : } private fun adjustFabVisibility( - isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true, + isResumeEnabled: Boolean = viewModel.isResumeEnabled.value, topFragment: Fragment? = navigationDelegate.primaryFragment, isSearchOpened: Boolean = isSearchOpened(), ) { - val fab = binding.fab ?: return + val fab = viewBinding.fab ?: return if ( isResumeEnabled && !actionModeDelegate.isActionModeStarted && @@ -360,27 +356,27 @@ class MainActivity : private fun adjustSearchUI(isOpened: Boolean, animate: Boolean) { if (animate) { - TransitionManager.beginDelayedTransition(binding.appbar) + TransitionManager.beginDelayedTransition(viewBinding.appbar) } val appBarScrollFlags = if (isOpened) { SCROLL_FLAG_NO_SCROLL } else { SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP } - binding.toolbarCard.updateLayoutParams { scrollFlags = appBarScrollFlags } - binding.insetsHolder.updateLayoutParams { scrollFlags = appBarScrollFlags } - binding.toolbarCard.background = if (isOpened) { + viewBinding.toolbarCard.updateLayoutParams { scrollFlags = appBarScrollFlags } + viewBinding.insetsHolder.updateLayoutParams { scrollFlags = appBarScrollFlags } + viewBinding.toolbarCard.background = if (isOpened) { null } else { ContextCompat.getDrawable(this, R.drawable.toolbar_background) } val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal) - binding.appbar.updatePadding(left = padding, right = padding) + viewBinding.appbar.updatePadding(left = padding, right = padding) adjustFabVisibility(isSearchOpened = isOpened) supportActionBar?.setHomeAsUpIndicator( if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, ) - showNav(!isOpened) + bottomNav?.showOrHide(!isOpened) } private fun requestNotificationsPermission() { @@ -396,7 +392,7 @@ class MainActivity : override fun onActivityResult(result: String?) { if (result != null) { - binding.searchView.query = result + viewBinding.searchView.query = result } } } @@ -405,7 +401,7 @@ class MainActivity : override fun handleOnBackPressed() { val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) - binding.searchView.clearFocus() + viewBinding.searchView.clearFocus() if (fragment == null) { // this should not happen but who knows isEnabled = false diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt index 92d71241d..adfec639a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.main.ui -import android.os.Bundle import android.view.MenuItem import androidx.activity.OnBackPressedCallback import androidx.annotation.IdRes @@ -10,13 +9,13 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import com.google.android.material.navigation.NavigationBarView import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.explore.ui.ExploreFragment import org.koitharu.kotatsu.settings.tools.ToolsFragment import org.koitharu.kotatsu.shelf.ui.ShelfFragment import org.koitharu.kotatsu.tracker.ui.feed.FeedFragment -import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled import java.util.LinkedList private const val TAG_PRIMARY = "primary" @@ -57,7 +56,7 @@ class MainNavigationDelegate( navBar.selectedItemId = R.id.nav_shelf } - fun onCreate(savedInstanceState: Bundle?) { + fun onCreate() { primaryFragment?.let { onFragmentChanged(it, fromUser = false) val itemId = getItemId(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt index 054b4cf5a..b983b7cbd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -5,40 +5,47 @@ import androidx.core.util.set import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.prefs.observeAsLiveData -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val historyRepository: HistoryRepository, private val appUpdateRepository: AppUpdateRepository, - private val trackingRepository: TrackingRepository, - private val settings: AppSettings, + trackingRepository: TrackingRepository, + settings: AppSettings, ) : BaseViewModel() { - val onOpenReader = SingleLiveEvent() + val onOpenReader = MutableEventFlow() val isResumeEnabled = combine( historyRepository.observeHasItems(), settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }, ) { hasItems, incognito -> hasItems && !incognito - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false, + ) - val isFeedAvailable = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val isFeedAvailable = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_TRACKER_ENABLED, valueProducer = { isTrackerEnabled }, ) @@ -51,7 +58,11 @@ class MainViewModel @Inject constructor( a[R.id.nav_tools] = if (appUpdate != null) 1 else 0 a[R.id.nav_feed] = tracks a - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0)) + }.stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.WhileSubscribed(5000), + initialValue = SparseIntArray(0), + ) init { launchJob { @@ -62,7 +73,7 @@ class MainViewModel @Inject constructor( fun openLastReader() { launchLoadingJob(Dispatchers.Default) { val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException() - onOpenReader.emitCall(manga) + onOpenReader.call(manga) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/AppBarOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/AppBarOwner.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/owners/AppBarOwner.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/AppBarOwner.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt similarity index 66% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt index 5174b97f4..ac6d1f0c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/BottomNavOwner.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.main.ui.owners -import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView +import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView interface BottomNavOwner { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt new file mode 100644 index 000000000..3920088d4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.main.ui.owners + +interface NoModalBottomSheetOwner { + + fun getBottomSheetCollapsedHeight(): Int +} diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/SnackbarOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/SnackbarOwner.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/owners/SnackbarOwner.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/SnackbarOwner.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt index 4d09d621a..8a9f8ed3c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/AppProtectHelper.kt @@ -4,13 +4,14 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import org.acra.dialog.CrashReportDialog -import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks import javax.inject.Inject import javax.inject.Singleton @Singleton -class AppProtectHelper @Inject constructor(private val settings: AppSettings) : DefaultActivityLifecycleCallbacks { +class AppProtectHelper @Inject constructor(private val settings: AppSettings) : + DefaultActivityLifecycleCallbacks { private var isUnlocked = settings.appPassword.isNullOrEmpty() diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt index d249c493a..ff6bf07bf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt @@ -19,10 +19,12 @@ import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.core.graphics.Insets import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivityProtectBinding -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat @AndroidEntryPoint class ProtectActivity : @@ -37,14 +39,14 @@ class ProtectActivity : super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) setContentView(ActivityProtectBinding.inflate(layoutInflater)) - binding.editPassword.setOnEditorActionListener(this) - binding.editPassword.addTextChangedListener(this) - binding.buttonNext.setOnClickListener(this) - binding.buttonCancel.setOnClickListener(this) + viewBinding.editPassword.setOnEditorActionListener(this) + viewBinding.editPassword.addTextChangedListener(this) + viewBinding.buttonNext.setOnClickListener(this) + viewBinding.buttonCancel.setOnClickListener(this) - viewModel.onError.observe(this, this::onError) + viewModel.onError.observeEvent(this, this::onError) viewModel.isLoading.observe(this, this::onLoadingStateChanged) - viewModel.onUnlockSuccess.observe(this) { + viewModel.onUnlockSuccess.observeEvent(this) { val intent = intent.getParcelableExtraCompat(EXTRA_INTENT) startActivity(intent) finishAfterTransition() @@ -54,13 +56,13 @@ class ProtectActivity : override fun onStart() { super.onStart() if (!useFingerprint()) { - binding.editPassword.requestFocus() + viewBinding.editPassword.requestFocus() } } override fun onWindowInsetsChanged(insets: Insets) { val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding) - binding.root.setPadding( + viewBinding.root.setPadding( basePadding + insets.left, basePadding + insets.top, basePadding + insets.right, @@ -70,14 +72,14 @@ class ProtectActivity : override fun onClick(v: View) { when (v.id) { - R.id.button_next -> viewModel.tryUnlock(binding.editPassword.text?.toString().orEmpty()) + R.id.button_next -> viewModel.tryUnlock(viewBinding.editPassword.text?.toString().orEmpty()) R.id.button_cancel -> finish() } } override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { - return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) { - binding.buttonNext.performClick() + return if (actionId == EditorInfo.IME_ACTION_DONE && viewBinding.buttonNext.isEnabled) { + viewBinding.buttonNext.performClick() true } else { false @@ -89,16 +91,16 @@ class ProtectActivity : override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable?) { - binding.layoutPassword.error = null - binding.buttonNext.isEnabled = !s.isNullOrEmpty() + viewBinding.layoutPassword.error = null + viewBinding.buttonNext.isEnabled = !s.isNullOrEmpty() } private fun onError(e: Throwable) { - binding.layoutPassword.error = e.getDisplayMessage(resources) + viewBinding.layoutPassword.error = e.getDisplayMessage(resources) } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.layoutPassword.isEnabled = !isLoading + viewBinding.layoutPassword.isEnabled = !isLoading } private fun useFingerprint(): Boolean { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt index a55d15c84..7f0d8f5b5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt @@ -1,14 +1,15 @@ package org.koitharu.kotatsu.main.ui.protect import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.parsers.util.md5 -import org.koitharu.kotatsu.utils.SingleLiveEvent +import javax.inject.Inject private const val PASSWORD_COMPARE_DELAY = 1_000L @@ -20,7 +21,7 @@ class ProtectViewModel @Inject constructor( private var job: Job? = null - val onUnlockSuccess = SingleLiveEvent() + val onUnlockSuccess = MutableEventFlow() val isBiometricEnabled get() = settings.isBiometricProtectionEnabled diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/ModelMapping.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/data/ModelMapping.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/ChapterPages.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPages.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/domain/ChapterPages.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChapterPages.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt index ecd163e36..1c75d5702 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ChaptersLoader.kt @@ -5,7 +5,7 @@ import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.details.domain.model.DoubleManga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import javax.inject.Inject @@ -17,17 +17,27 @@ class ChaptersLoader @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) { - val chapters = LongSparseArray() + private val chapters = LongSparseArray() private val chapterPages = ChapterPages() private val mutex = Mutex() - suspend fun loadPrevNextChapter(manga: Manga, currentId: Long, isNext: Boolean) { + val size: Int + get() = chapters.size() + + suspend fun init(manga: DoubleManga) = mutex.withLock { + chapters.clear() + manga.chapters?.forEach { + chapters.put(it.id, it) + } + } + + suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) { val chapters = manga.chapters ?: return val predicate: (MangaChapter) -> Boolean = { it.id == currentId } val index = if (isNext) chapters.indexOfFirst(predicate) else chapters.indexOfLast(predicate) if (index == -1) return val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return - val newPages = loadChapter(manga, newChapter.id) + val newPages = loadChapter(newChapter.id) mutex.withLock { if (chapterPages.chaptersSize > 1) { // trim pages @@ -47,14 +57,16 @@ class ChaptersLoader @Inject constructor( } } - suspend fun loadSingleChapter(manga: Manga, chapterId: Long) { - val pages = loadChapter(manga, chapterId) + suspend fun loadSingleChapter(chapterId: Long) { + val pages = loadChapter(chapterId) mutex.withLock { chapterPages.clear() chapterPages.addLast(chapterId, pages) } } + fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId] + fun getPages(chapterId: Long): List { return chapterPages.subList(chapterId) } @@ -63,11 +75,15 @@ class ChaptersLoader @Inject constructor( return chapterPages.size(chapterId) } + fun last() = chapterPages.last() + + fun first() = chapterPages.first() + fun snapshot() = chapterPages.toList() - private suspend fun loadChapter(manga: Manga, chapterId: Long): List { + private suspend fun loadChapter(chapterId: Long): List { val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } - val repo = mangaRepositoryFactory.create(manga.source) + val repo = mangaRepositoryFactory.create(chapter.source) return repo.getPages(chapter).mapIndexed { index, page -> ReaderPage(page, index, chapterId) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt new file mode 100644 index 000000000..049914d3c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -0,0 +1,98 @@ +package org.koitharu.kotatsu.reader.domain + +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Size +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import okhttp3.OkHttpClient +import org.koitharu.kotatsu.core.model.findChapter +import org.koitharu.kotatsu.core.network.ImageProxyInterceptor +import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import java.io.InputStream +import java.util.zip.ZipFile +import javax.inject.Inject +import kotlin.math.roundToInt + +class DetectReaderModeUseCase @Inject constructor( + private val dataRepository: MangaDataRepository, + private val settings: AppSettings, + private val mangaRepositoryFactory: MangaRepository.Factory, + @MangaHttpClient private val okHttpClient: OkHttpClient, + private val imageProxyInterceptor: ImageProxyInterceptor, +) { + + suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode { + dataRepository.getReaderMode(manga.id)?.let { return it } + val defaultMode = settings.defaultReaderMode + if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) { + return defaultMode + } + val chapter = state?.let { manga.findChapter(it.chapterId) } + ?: manga.chapters?.firstOrNull() + ?: error("There are no chapters in this manga") + val repo = mangaRepositoryFactory.create(manga.source) + val pages = repo.getPages(chapter) + return runCatchingCancellable { + val isWebtoon = guessMangaIsWebtoon(repo, pages) + if (isWebtoon) ReaderMode.WEBTOON else defaultMode + }.onSuccess { + dataRepository.saveReaderMode(manga, it) + }.onFailure { + it.printStackTraceDebug() + }.getOrDefault(defaultMode) + } + + /** + * Automatic determine type of manga by page size + * @return ReaderMode.WEBTOON if page is wide + */ + private suspend fun guessMangaIsWebtoon(repository: MangaRepository, pages: List): Boolean { + val pageIndex = (pages.size * 0.3).roundToInt() + val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" } + val url = repository.getPageUrl(page) + val uri = Uri.parse(url) + val size = if (uri.scheme == "cbz") { + runInterruptible(Dispatchers.IO) { + val zip = ZipFile(uri.schemeSpecificPart) + val entry = zip.getEntry(uri.fragment) + zip.getInputStream(entry).use { + getBitmapSize(it) + } + } + } else { + val request = PageLoader.createPageRequest(page, url) + imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { + runInterruptible(Dispatchers.IO) { + getBitmapSize(it.body?.byteStream()) + } + } + } + return size.width * MIN_WEBTOON_RATIO < size.height + } + + companion object { + + private const val MIN_WEBTOON_RATIO = 1.8 + + private fun getBitmapSize(input: InputStream?): Size { + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeStream(input, null, options)?.recycle() + val imageHeight: Int = options.outHeight + val imageWidth: Int = options.outWidth + check(imageHeight > 0 && imageWidth > 0) + return Size(imageWidth, imageHeight) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 67127d854..48e5faf0e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -12,6 +12,7 @@ import dagger.hilt.android.lifecycle.RetainedLifecycle import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -24,20 +25,24 @@ import okhttp3.OkHttpClient import okhttp3.Request import okio.source import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.network.ImageProxyInterceptor +import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope +import org.koitharu.kotatsu.core.util.ext.ensureSuccess +import org.koitharu.kotatsu.core.util.ext.isNotEmpty +import org.koitharu.kotatsu.core.util.ext.ramAvailable +import org.koitharu.kotatsu.core.util.ext.withProgress +import org.koitharu.kotatsu.core.util.progress.ProgressDeferred +import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.ramAvailable -import org.koitharu.kotatsu.utils.ext.withProgress -import org.koitharu.kotatsu.utils.progress.ProgressDeferred +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger @@ -46,18 +51,15 @@ import javax.inject.Inject import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext -private const val PROGRESS_UNDEFINED = -1f -private const val PREFETCH_LIMIT_DEFAULT = 10 -private const val PREFETCH_MIN_RAM_MB = 80L - @ActivityRetainedScoped class PageLoader @Inject constructor( @ApplicationContext private val context: Context, lifecycle: ActivityRetainedLifecycle, - private val okHttp: OkHttpClient, + @MangaHttpClient private val okHttp: OkHttpClient, private val cache: PagesCache, private val settings: AppSettings, private val mangaRepositoryFactory: MangaRepository.Factory, + private val imageProxyInterceptor: ImageProxyInterceptor, ) : RetainedLifecycle.OnClearedListener { init { @@ -81,7 +83,7 @@ class PageLoader @Inject constructor( } fun isPrefetchApplicable(): Boolean { - return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() && !isLowRam() + return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled && !isLowRam() } @AnyThread @@ -103,7 +105,7 @@ class PageLoader @Inject constructor( } fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred { - var task = tasks[page.id] + var task = tasks[page.id]?.takeIf { it.isValid() } if (force) { task?.cancel() } else if (task?.isCancelled == false) { @@ -188,7 +190,7 @@ class PageLoader @Inject constructor( val pageUrl = getPageUrl(page) check(pageUrl.isNotBlank()) { "Cannot obtain full image url" } val uri = Uri.parse(pageUrl) - return if (uri.scheme == "cbz") { + return if (CbzFilter.isUriSupported(uri)) { runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart) }.use { zip -> @@ -200,17 +202,8 @@ class PageLoader @Inject constructor( } } } else { - val request = Request.Builder() - .url(pageUrl) - .get() - .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8") - .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) - .tag(MangaSource::class.java, page.source) - .build() - okHttp.newCall(request).await().use { response -> - check(response.isSuccessful) { - "Invalid response: ${response.code} ${response.message} at $pageUrl" - } + val request = createPageRequest(page, pageUrl) + imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> val body = checkNotNull(response.body) { "Null response" } @@ -225,12 +218,35 @@ class PageLoader @Inject constructor( return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) } + private fun Deferred.isValid(): Boolean { + return if (isCompleted) { + val file = getCompleted() + file.exists() && file.isNotEmpty() + } else { + true + } + } + private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { override fun handleException(context: CoroutineContext, exception: Throwable) { exception.printStackTraceDebug() } + } + + companion object { + + private const val PROGRESS_UNDEFINED = -1f + private const val PREFETCH_LIMIT_DEFAULT = 10 + private const val PREFETCH_MIN_RAM_MB = 80L + fun createPageRequest(page: MangaPage, pageUrl: String) = Request.Builder() + .url(pageUrl) + .get() + .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8") + .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) + .tag(MangaSource::class.java, page.source) + .build() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt similarity index 64% rename from app/src/main/java/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt index 1511a29fc..8852ff2da 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/ReaderColorFilter.kt @@ -6,19 +6,52 @@ import android.graphics.ColorMatrixColorFilter class ReaderColorFilter( val brightness: Float, val contrast: Float, + val isInverted: Boolean, ) { val isEmpty: Boolean - get() = brightness == 0f && contrast == 0f + get() = !isInverted && brightness == 0f && contrast == 0f fun toColorFilter(): ColorMatrixColorFilter { val cm = ColorMatrix() - val scale = brightness + 1f - cm.setScale(scale, scale, scale, 1f) + if (isInverted) { + cm.inverted() + } + cm.setBrightness(brightness) cm.setContrast(contrast) return ColorMatrixColorFilter(cm) } + private fun ColorMatrix.setBrightness(brightness: Float) { + val scale = brightness + 1f + val matrix = ColorMatrix() + matrix.setScale(scale, scale, scale, 1f) + postConcat(matrix) + } + + private fun ColorMatrix.setContrast(contrast: Float) { + val scale = contrast + 1f + val translate = (-.5f * scale + .5f) * 255f + val array = floatArrayOf( + scale, 0f, 0f, 0f, translate, + 0f, scale, 0f, 0f, translate, + 0f, 0f, scale, 0f, translate, + 0f, 0f, 0f, 1f, 0f, + ) + val matrix = ColorMatrix(array) + postConcat(matrix) + } + + private fun ColorMatrix.inverted() { + val matrix = floatArrayOf( + -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, + 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, + 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, + ) + set(matrix) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -27,26 +60,13 @@ class ReaderColorFilter( if (brightness != other.brightness) return false if (contrast != other.contrast) return false - - return true + return isInverted == other.isInverted } override fun hashCode(): Int { var result = brightness.hashCode() result = 31 * result + contrast.hashCode() + result = 31 * result + isInverted.hashCode() return result } - - private fun ColorMatrix.setContrast(contrast: Float) { - val scale = contrast + 1f - val translate = (-.5f * scale + .5f) * 255f - val array = floatArrayOf( - scale, 0f, 0f, 0f, translate, - 0f, scale, 0f, 0f, translate, - 0f, 0f, scale, 0f, translate, - 0f, 0f, 0f, 1f, 0f, - ) - val matrix = ColorMatrix(array) - postConcat(matrix) - } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt index 87b92acd5..4dcfce2e8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersSheet.kt @@ -7,33 +7,33 @@ import android.view.ViewGroup import androidx.fragment.app.FragmentManager 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.parcelable.ParcelableMangaChapters import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback +import org.koitharu.kotatsu.core.util.ext.getParcelableCompat +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetChaptersBinding import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback -import org.koitharu.kotatsu.utils.ext.getParcelableCompat -import org.koitharu.kotatsu.utils.ext.withArgs import javax.inject.Inject import kotlin.math.roundToInt @AndroidEntryPoint -class ChaptersBottomSheet : BaseBottomSheet(), OnListItemClickListener { +class ChaptersSheet : BaseAdaptiveSheet(), OnListItemClickListener { @Inject lateinit var settings: AppSettings - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetChaptersBinding { return SheetChaptersBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val chapters = arguments?.getParcelableCompat(ARG_CHAPTERS)?.chapters if (chapters.isNullOrEmpty()) { dismissAllowingStateLoss() @@ -46,8 +46,8 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC isCurrent = index == currentPosition, isUnread = index > currentPosition, isNew = false, - isMissing = false, isDownloaded = false, + isBookmarked = false, ) } binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter -> @@ -84,7 +84,7 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC fm: FragmentManager, chapters: List, currentId: Long, - ) = ChaptersBottomSheet().withArgs(2) { + ) = ChaptersSheet().withArgs(2) { putParcelable(ARG_CHAPTERS, ParcelableMangaChapters(chapters)) putLong(ARG_CURRENT_ID, currentId) }.show(fm, TAG) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLabelFormatter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageLabelFormatter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/PageLabelFormatter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageLabelFormatter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index 9f89886e9..b312c9b12 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context +import android.graphics.BitmapFactory import android.net.Uri import android.webkit.MimeTypeMap import androidx.activity.result.ActivityResultLauncher @@ -15,11 +16,10 @@ import okio.IOException import okio.buffer import okio.sink import okio.source -import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.toFileNameSafe import org.koitharu.kotatsu.reader.domain.PageLoader -import org.koitharu.kotatsu.utils.ext.writeAllCancellable import java.io.File import javax.inject.Inject import kotlin.coroutines.Continuation @@ -74,7 +74,7 @@ class PageSaveHelper @Inject constructor( var extension = name.substringAfterLast('.', "") name = name.substringBeforeLast('.') if (extension.length !in 2..4) { - val mimeType = MangaDataRepository.getImageMimeType(file) + val mimeType = getImageMimeType(file) extension = if (mimeType != null) { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK } else { @@ -83,4 +83,12 @@ class PageSaveHelper @Inject constructor( } return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension } + + private suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(file.path, options)?.recycle() + options.outMimeType + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index e9a314b48..c33f1b076 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -14,6 +14,7 @@ import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View +import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager import androidx.activity.viewModels import androidx.core.graphics.Insets @@ -21,6 +22,7 @@ import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.WindowInsetsCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar @@ -30,40 +32,42 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.base.ui.BaseFullscreenActivity import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.core.exceptions.resolve.DialogErrorObserver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity +import org.koitharu.kotatsu.core.util.GridTouchHelper +import org.koitharu.kotatsu.core.util.IdlingDetector +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint +import org.koitharu.kotatsu.core.util.ext.isRtl +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.postDelayed +import org.koitharu.kotatsu.core.util.ext.setValueRounded +import org.koitharu.kotatsu.core.util.ext.zipWithPrevious import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.ui.config.ReaderConfigBottomSheet +import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.utils.GridTouchHelper -import org.koitharu.kotatsu.utils.IdlingDetector -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.hasGlobalPoint -import org.koitharu.kotatsu.utils.ext.isRtl -import org.koitharu.kotatsu.utils.ext.observeWithPrevious -import org.koitharu.kotatsu.utils.ext.postDelayed -import org.koitharu.kotatsu.utils.ext.setValueRounded import java.util.concurrent.TimeUnit import javax.inject.Inject @AndroidEntryPoint class ReaderActivity : BaseFullscreenActivity(), - ChaptersBottomSheet.OnChapterChangeListener, + ChaptersSheet.OnChapterChangeListener, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, - ReaderConfigBottomSheet.Callback, + ReaderConfigSheet.Callback, ReaderControlDelegate.OnInteractionListener, OnApplyWindowInsetsListener, IdlingDetector.Callback { @@ -102,40 +106,40 @@ class ReaderActivity : touchHelper = GridTouchHelper(this, this) scrollTimer = scrollTimerFactory.create(this, this) controlDelegate = ReaderControlDelegate(settings, this, this) - binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) - binding.slider.setLabelFormatter(PageLabelFormatter()) - ReaderSliderListener(this, viewModel).attachToSlider(binding.slider) + viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) + viewBinding.slider.setLabelFormatter(PageLabelFormatter()) + ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider) insetsDelegate.interceptingWindowInsetsListener = this idlingDetector.bindToLifecycle(this) - viewModel.onError.observe( + viewModel.onError.observeEvent( this, DialogErrorObserver( - host = binding.container, + host = viewBinding.container, fragment = null, resolver = exceptionResolver, onResolved = { isResolved -> if (isResolved) { viewModel.reload() - } else if (viewModel.content.value?.pages.isNullOrEmpty()) { + } else if (viewModel.content.value.pages.isEmpty()) { finishAfterTransition() } }, ), ) viewModel.readerMode.observe(this, this::onInitReader) - viewModel.onPageSaved.observe(this, this::onPageSaved) - viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged) + viewModel.onPageSaved.observeEvent(this, this::onPageSaved) + viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.content.observe(this) { - onLoadingStateChanged(viewModel.isLoading.value == true) + onLoadingStateChanged(viewModel.isLoading.value) } viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure) viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged) viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged) - viewModel.onShowToast.observe(this) { msgId -> - Snackbar.make(binding.container, msgId, Snackbar.LENGTH_SHORT) - .setAnchorView(binding.appbarBottom) + viewModel.onShowToast.observeEvent(this) { msgId -> + Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT) + .setAnchorView(viewBinding.appbarBottom) .show() } } @@ -150,14 +154,17 @@ class ReaderActivity : viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) } - private fun onInitReader(mode: ReaderMode) { + private fun onInitReader(mode: ReaderMode?) { + if (mode == null) { + return + } if (readerManager.currentMode != mode) { readerManager.replace(mode) } - if (binding.appbarTop.isVisible) { + if (viewBinding.appbarTop.isVisible) { lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1)) } - binding.slider.isRtl = mode == ReaderMode.REVERSED + viewBinding.slider.isRtl = mode == ReaderMode.REVERSED } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -172,7 +179,7 @@ class ReaderActivity : } R.id.action_chapters -> { - ChaptersBottomSheet.show( + ChaptersSheet.show( supportFragmentManager, viewModel.manga?.chapters.orEmpty(), viewModel.getCurrentState()?.chapterId ?: 0L, @@ -180,21 +187,17 @@ class ReaderActivity : } R.id.action_pages_thumbs -> { - val pages = viewModel.getCurrentChapterPages() - if (!pages.isNullOrEmpty()) { - PagesThumbnailsSheet.show( - supportFragmentManager, - pages, - title?.toString().orEmpty(), - readerManager.currentReader?.getCurrentState()?.page ?: -1, - ) - } else { - return false - } + val state = viewModel.getCurrentState() ?: return false + PagesThumbnailsSheet.show( + supportFragmentManager, + viewModel.manga?.any ?: return false, + state.chapterId, + state.page, + ) } R.id.action_bookmark -> { - if (viewModel.isBookmarkAdded.value == true) { + if (viewModel.isBookmarkAdded.value) { viewModel.removeBookmark() } else { viewModel.addBookmark() @@ -204,7 +207,7 @@ class ReaderActivity : R.id.action_options -> { viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) val currentMode = readerManager.currentMode ?: return false - ReaderConfigBottomSheet.show(supportFragmentManager, currentMode) + ReaderConfigSheet.show(supportFragmentManager, currentMode) } else -> return super.onOptionsItemSelected(item) @@ -213,30 +216,30 @@ class ReaderActivity : } private fun onLoadingStateChanged(isLoading: Boolean) { - val hasPages = !viewModel.content.value?.pages.isNullOrEmpty() - binding.layoutLoading.isVisible = isLoading && !hasPages + val hasPages = viewModel.content.value.pages.isNotEmpty() + viewBinding.layoutLoading.isVisible = isLoading && !hasPages if (isLoading && hasPages) { - binding.toastView.show(R.string.loading_) + viewBinding.toastView.show(R.string.loading_) } else { - binding.toastView.hide() + viewBinding.toastView.hide() } - val menu = binding.toolbarBottom.menu + val menu = viewBinding.toolbarBottom.menu menu.findItem(R.id.action_bookmark).isVisible = hasPages menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages } override fun onGridTouch(area: Int) { - controlDelegate.onGridTouch(area, binding.container) + controlDelegate.onGridTouch(area, viewBinding.container) } override fun onProcessTouch(rawX: Int, rawY: Int): Boolean { return if ( rawX <= gestureInsets.left || rawY <= gestureInsets.top || - rawX >= binding.root.width - gestureInsets.right || - rawY >= binding.root.height - gestureInsets.bottom || - binding.appbarTop.hasGlobalPoint(rawX, rawY) || - binding.appbarBottom?.hasGlobalPoint(rawX, rawY) == true + rawX >= viewBinding.root.width - gestureInsets.right || + rawY >= viewBinding.root.height - gestureInsets.bottom || + viewBinding.appbarTop.hasGlobalPoint(rawX, rawY) || + viewBinding.appbarBottom?.hasGlobalPoint(rawX, rawY) == true ) { false } else { @@ -259,17 +262,19 @@ class ReaderActivity : } override fun onChapterChanged(chapter: MangaChapter) { - viewModel.switchChapter(chapter.id) + viewModel.switchChapter(chapter.id, 0) } - override fun onPageSelected(page: MangaPage) { + override fun onPageSelected(page: ReaderPage) { lifecycleScope.launch(Dispatchers.Default) { - val pages = viewModel.content.value?.pages ?: return@launch - val index = pages.indexOfFirst { it.id == page.id } + val pages = viewModel.content.value.pages + val index = pages.indexOfFirst { it.chapterId == page.chapterId && it.id == page.id } if (index != -1) { withContext(Dispatchers.Main) { readerManager.currentReader?.switchPageTo(index, true) } + } else { + viewModel.switchChapter(page.chapterId, page.index) } } } @@ -281,14 +286,14 @@ class ReaderActivity : private fun onPageSaved(uri: Uri?) { if (uri != null) { - Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_LONG) - .setAnchorView(binding.appbarBottom) + Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG) + .setAnchorView(viewBinding.appbarBottom) .setAction(R.string.share) { ShareHelper(this).shareImage(uri) }.show() } else { - Snackbar.make(binding.container, R.string.error_occurred, Snackbar.LENGTH_SHORT) - .setAnchorView(binding.appbarBottom) + Snackbar.make(viewBinding.container, R.string.error_occurred, Snackbar.LENGTH_SHORT) + .setAnchorView(viewBinding.appbarBottom) .show() } } @@ -302,18 +307,18 @@ class ReaderActivity : } private fun setUiIsVisible(isUiVisible: Boolean) { - if (binding.appbarTop.isVisible != isUiVisible) { + if (viewBinding.appbarTop.isVisible != isUiVisible) { val transition = TransitionSet() .setOrdering(TransitionSet.ORDERING_TOGETHER) - .addTransition(Slide(Gravity.TOP).addTarget(binding.appbarTop)) - .addTransition(Fade().addTarget(binding.infoBar)) - binding.appbarBottom?.let { bottomBar -> + .addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop)) + .addTransition(Fade().addTarget(viewBinding.infoBar)) + viewBinding.appbarBottom?.let { bottomBar -> transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar)) } - TransitionManager.beginDelayedTransition(binding.root, transition) - binding.appbarTop.isVisible = isUiVisible - binding.appbarBottom?.isVisible = isUiVisible - binding.infoBar.isGone = isUiVisible || (viewModel.isInfoBarEnabled.value == false) + TransitionManager.beginDelayedTransition(viewBinding.root, transition) + viewBinding.appbarTop.isVisible = isUiVisible + viewBinding.appbarBottom?.isVisible = isUiVisible + viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value) if (isUiVisible) { showSystemUI() } else { @@ -325,16 +330,16 @@ class ReaderActivity : override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { gestureInsets = insets.getInsets(WindowInsetsCompat.Type.systemGestures()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - binding.appbarTop.updatePadding( + viewBinding.appbarTop.updatePadding( top = systemBars.top, right = systemBars.right, left = systemBars.left, ) - binding.appbarBottom?.updatePadding( - bottom = systemBars.bottom, - right = systemBars.right, - left = systemBars.left, - ) + viewBinding.appbarBottom?.updateLayoutParams { + bottomMargin = systemBars.bottom + topMargin + rightMargin = systemBars.right + topMargin + leftMargin = systemBars.left + topMargin + } return WindowInsetsCompat.Builder(insets) .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE) .build() @@ -351,7 +356,7 @@ class ReaderActivity : } override fun toggleUiVisibility() { - setUiIsVisible(!binding.appbarTop.isVisible) + setUiIsVisible(!viewBinding.appbarTop.isVisible) } override fun isReaderResumed(): Boolean { @@ -360,81 +365,83 @@ class ReaderActivity : } private fun onReaderBarChanged(isBarEnabled: Boolean) { - binding.infoBar.isVisible = isBarEnabled && binding.appbarTop.isGone + viewBinding.infoBar.isVisible = isBarEnabled && viewBinding.appbarTop.isGone } private fun onBookmarkStateChanged(isAdded: Boolean) { - val menuItem = binding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return + val menuItem = viewBinding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return menuItem.setTitle(if (isAdded) R.string.bookmark_remove else R.string.bookmark_add) menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark) } - private fun onUiStateChanged(uiState: ReaderUiState?, previous: ReaderUiState?) { - title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_) - binding.infoBar.update(uiState) + private fun onUiStateChanged(pair: Pair) { + val (previous: ReaderUiState?, uiState: ReaderUiState?) = pair + title = uiState?.resolveTitle(this) ?: getString(R.string.loading_) + viewBinding.infoBar.update(uiState) if (uiState == null) { supportActionBar?.subtitle = null - binding.slider.isVisible = false + viewBinding.slider.isVisible = false return } - supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) { - getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal) - } else { - null - } + supportActionBar?.subtitle = uiState.chapterName if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) { if (!uiState.chapterName.isNullOrEmpty()) { - binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION) + viewBinding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION) } } if (uiState.isSliderAvailable()) { - binding.slider.valueTo = uiState.totalPages.toFloat() - 1 - binding.slider.setValueRounded(uiState.currentPage.toFloat()) - binding.slider.isVisible = true + viewBinding.slider.valueTo = uiState.totalPages.toFloat() - 1 + viewBinding.slider.setValueRounded(uiState.currentPage.toFloat()) + viewBinding.slider.isVisible = true } else { - binding.slider.isVisible = false + viewBinding.slider.isVisible = false } } - companion object { + class IntentBuilder(context: Context) { - const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA" - const val EXTRA_STATE = "state" - const val EXTRA_BRANCH = "branch" - const val EXTRA_INCOGNITO = "incognito" - private const val TOAST_DURATION = 1500L + private val intent = Intent(context, ReaderActivity::class.java) + .setAction(ACTION_MANGA_READ) + + fun manga(manga: Manga) = apply { + intent.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) + } + + fun mangaId(mangaId: Long) = apply { + intent.putExtra(MangaIntent.KEY_ID, mangaId) + } - fun newIntent(context: Context, manga: Manga): Intent { - return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) + fun incognito(incognito: Boolean) = apply { + intent.putExtra(EXTRA_INCOGNITO, incognito) } - fun newIntent(context: Context, manga: Manga, branch: String?, isIncognitoMode: Boolean): Intent { - return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) - .putExtra(EXTRA_BRANCH, branch) - .putExtra(EXTRA_INCOGNITO, isIncognitoMode) + fun branch(branch: String?) = apply { + intent.putExtra(EXTRA_BRANCH, branch) } - fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent { - return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) - .putExtra(EXTRA_STATE, state) + fun state(state: ReaderState?) = apply { + intent.putExtra(EXTRA_STATE, state) } - fun newIntent(context: Context, bookmark: Bookmark): Intent { - val state = ReaderState( + fun bookmark(bookmark: Bookmark) = manga( + bookmark.manga, + ).state( + ReaderState( chapterId = bookmark.chapterId, page = bookmark.page, scroll = bookmark.scroll, - ) - return newIntent(context, bookmark.manga, state) - .putExtra(EXTRA_INCOGNITO, true) - } + ), + ) - fun newIntent(context: Context, mangaId: Long): Intent { - return Intent(context, ReaderActivity::class.java) - .putExtra(MangaIntent.KEY_ID, mangaId) - } + fun build() = intent + } + + companion object { + + const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA" + const val EXTRA_STATE = "state" + const val EXTRA_BRANCH = "branch" + const val EXTRA_INCOGNITO = "incognito" + private const val TOAST_DURATION = 1500L } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderContent.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderContent.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderContent.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index 3791ac5c4..62c6cd431 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode -import org.koitharu.kotatsu.utils.GridTouchHelper +import org.koitharu.kotatsu.core.util.GridTouchHelper class ReaderControlDelegate( private val settings: AppSettings, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt index f4fd28289..e0306c6fb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderInfoBarView.kt @@ -17,12 +17,12 @@ import androidx.core.graphics.ColorUtils import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.measureDimension +import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.measureDimension -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.resolveDp +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.text.SimpleDateFormat import java.util.Date import com.google.android.material.R as materialR diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt index bd959969d..50e87a4d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.reader.ui import com.google.android.material.slider.Slider +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener class ReaderSliderListener( @@ -41,6 +42,7 @@ class ReaderSliderListener( private fun switchPageToIndex(index: Int) { val pages = viewModel.getCurrentChapterPages() val page = pages?.getOrNull(index) ?: return - pageSelectListener.onPageSelected(page) + val chapterId = viewModel.getCurrentState()?.chapterId ?: return + pageSelectListener.onPageSelected(ReaderPage(page, index, chapterId)) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderState.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderState.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index da7f04e5b..3a260db4f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -1,13 +1,10 @@ package org.koitharu.kotatsu.reader.ui import android.net.Uri -import android.util.LongSparseArray import androidx.activity.result.ActivityResultLauncher import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -16,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -28,39 +26,37 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.os.ShortcutsUpdater -import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.prefs.observeAsLiveData -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.requireValue +import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase +import org.koitharu.kotatsu.details.domain.model.DoubleManga +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.history.data.PROGRESS_NONE +import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.domain.ChaptersLoader +import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.emitValue -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.requireValue -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import java.util.Date import javax.inject.Inject @@ -70,7 +66,6 @@ private const val PREFETCH_LIMIT = 10 @HiltViewModel class ReaderViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val mangaRepositoryFactory: MangaRepository.Factory, private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, private val bookmarksRepository: BookmarksRepository, @@ -79,6 +74,9 @@ class ReaderViewModel @Inject constructor( private val pageLoader: PageLoader, private val chaptersLoader: ChaptersLoader, private val shortcutsUpdater: ShortcutsUpdater, + private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, + private val historyUpdateUseCase: HistoryUpdateUseCase, + private val detectReaderModeUseCase: DetectReaderModeUseCase, ) : BaseViewModel() { private val intent = MangaIntent(savedStateHandle) @@ -90,33 +88,33 @@ class ReaderViewModel @Inject constructor( private var bookmarkJob: Job? = null private var stateChangeJob: Job? = null private val currentState = MutableStateFlow(savedStateHandle[ReaderActivity.EXTRA_STATE]) - private val mangaData = MutableStateFlow(intent.manga) - private val chapters: LongSparseArray - get() = chaptersLoader.chapters + private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) }) + private val mangaFlow: Flow + get() = mangaData.map { it?.any } - val readerMode = MutableLiveData() - val onPageSaved = SingleLiveEvent() - val onShowToast = SingleLiveEvent() - val uiState = MutableLiveData(null) + val readerMode = MutableStateFlow(null) + val onPageSaved = MutableEventFlow() + val onShowToast = MutableEventFlow() + val uiState = MutableStateFlow(null) - val content = MutableLiveData(ReaderContent(emptyList(), null)) - val manga: Manga? + val content = MutableStateFlow(ReaderContent(emptyList(), null)) + val manga: DoubleManga? get() = mangaData.value - val readerAnimation = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val readerAnimation = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_READER_ANIMATION, valueProducer = { readerAnimation }, ) - val isInfoBarEnabled = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val isInfoBarEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_READER_BAR, valueProducer = { isReaderBarEnabled }, ) - val isWebtoonZoomEnabled = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val isWebtoonZoomEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_WEBTOON_ZOOM, valueProducer = { isWebtoonZoomEnable }, ) @@ -124,28 +122,30 @@ class ReaderViewModel @Inject constructor( val readerSettings = ReaderSettings( parentScope = viewModelScope, settings = settings, - colorFilterFlow = mangaData.flatMapLatest { + colorFilterFlow = mangaFlow.flatMapLatest { if (it == null) flowOf(null) else dataRepository.observeColorFilter(it.id) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null), ) val isScreenshotsBlockEnabled = combine( - mangaData, + mangaFlow, settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }, ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) - val isBookmarkAdded: LiveData = currentState.flatMapLatest { state -> - val manga = mangaData.value + val isBookmarkAdded = currentState.flatMapLatest { state -> + val manga = mangaData.value?.any if (state == null || manga == null) { flowOf(false) } else { bookmarksRepository.observeBookmark(manga, state.chapterId, state.page) - .map { it != null } + .map { + it != null && it.chapterId == state.chapterId && it.page == state.page + } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) init { loadImpl() @@ -154,7 +154,7 @@ class ReaderViewModel @Inject constructor( if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged() }.launchIn(viewModelScope + Dispatchers.Default) launchJob(Dispatchers.Default) { - val mangaId = mangaData.filterNotNull().first().id + val mangaId = mangaFlow.filterNotNull().first().id shortcutsUpdater.notifyMangaOpened(mangaId) } } @@ -166,16 +166,14 @@ class ReaderViewModel @Inject constructor( fun switchMode(newMode: ReaderMode) { launchJob { - val manga = checkNotNull(mangaData.value) + val manga = checkNotNull(mangaData.value?.any) dataRepository.saveReaderMode( manga = manga, mode = newMode, ) readerMode.value = newMode - content.value?.run { - content.value = copy( - state = getCurrentState(), - ) + content.update { + it.copy(state = getCurrentState()) } } } @@ -188,9 +186,9 @@ class ReaderViewModel @Inject constructor( return } val readerState = state ?: currentState.value ?: return - historyRepository.saveStateAsync( - manga = mangaData.value ?: return, - state = readerState, + historyUpdateUseCase.invokeAsync( + manga = mangaData.value?.any ?: return, + readerState = readerState, percent = computePercent(readerState.chapterId, readerState.page), ) } @@ -211,12 +209,12 @@ class ReaderViewModel @Inject constructor( prevJob?.cancelAndJoin() try { val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher) - onPageSaved.emitCall(dest) + onPageSaved.call(dest) } catch (e: CancellationException) { throw e } catch (e: Exception) { e.printStackTraceDebug() - onPageSaved.emitCall(null) + onPageSaved.call(null) } } } @@ -232,18 +230,18 @@ class ReaderViewModel @Inject constructor( fun getCurrentPage(): MangaPage? { val state = currentState.value ?: return null - return content.value?.pages?.find { + return content.value.pages.find { it.chapterId == state.chapterId && it.index == state.page }?.toMangaPage() } - fun switchChapter(id: Long) { + fun switchChapter(id: Long, page: Int) { val prevJob = loadingJob loadingJob = launchLoadingJob(Dispatchers.Default) { prevJob?.cancelAndJoin() - content.postValue(ReaderContent(emptyList(), null)) - chaptersLoader.loadSingleChapter(mangaData.requireValue(), id) - content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, 0, 0))) + content.value = ReaderContent(emptyList(), null) + chaptersLoader.loadSingleChapter(id) + content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)) } } @@ -253,7 +251,7 @@ class ReaderViewModel @Inject constructor( stateChangeJob = launchJob(Dispatchers.Default) { prevJob?.cancelAndJoin() loadingJob?.join() - val pages = content.value?.pages ?: return@launchJob + val pages = content.value.pages pages.getOrNull(position)?.let { page -> currentState.update { cs -> cs?.copy(chapterId = page.chapterId, page = page.index) @@ -285,17 +283,17 @@ class ReaderViewModel @Inject constructor( val state = checkNotNull(currentState.value) val page = checkNotNull(getCurrentPage()) { "Page not found" } val bookmark = Bookmark( - manga = checkNotNull(mangaData.value), + manga = checkNotNull(mangaData.value?.any), pageId = page.id, chapterId = state.chapterId, page = state.page, scroll = state.scroll, - imageUrl = page.preview ?: pageLoader.getPageUrl(page), + imageUrl = page.preview.ifNullOrEmpty { page.url }, createdAt = Date(), percent = computePercent(state.chapterId, state.page), ) bookmarksRepository.addBookmark(bookmark) - onShowToast.emitCall(R.string.bookmark_added) + onShowToast.call(R.string.bookmark_added) } } @@ -305,45 +303,45 @@ class ReaderViewModel @Inject constructor( } bookmarkJob = launchJob { loadingJob?.join() - val manga = checkNotNull(mangaData.value) - val page = checkNotNull(getCurrentPage()) { "Page not found" } - bookmarksRepository.removeBookmark(manga.id, page.id) + val manga = checkNotNull(mangaData.value?.any) + val state = checkNotNull(getCurrentState()) + bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page) onShowToast.call(R.string.bookmark_removed) } } private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { - var manga = dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "") + var manga = DoubleManga( + dataRepository.resolveIntent(intent) + ?: throw NotFoundException("Cannot find manga", ""), + ) mangaData.value = manga - val repo = mangaRepositoryFactory.create(manga.source) - manga = repo.getDetails(manga) - manga.chapters?.forEach { - chapters.put(it.id, it) - } + manga = doubleMangaLoadUseCase(intent) + chaptersLoader.init(manga) // determine mode - val mode = detectReaderMode(manga, repo) + val singleManga = manga.requireAny() // obtain state if (currentState.value == null) { - currentState.value = historyRepository.getOne(manga)?.let { + currentState.value = historyRepository.getOne(singleManga)?.let { ReaderState(it) - } ?: ReaderState(manga, preselectedBranch) + } ?: ReaderState(singleManga, preselectedBranch) } - - val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch + val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value) + val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch mangaData.value = manga.filterChapters(branch) - readerMode.emitValue(mode) + readerMode.value = mode - chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId) + chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId) // save state if (!isIncognito) { currentState.value?.let { val percent = computePercent(it.chapterId, it.page) - historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) + historyUpdateUseCase.invoke(singleManga, it, percent) } } notifyStateChanged() - content.emitValue(ReaderContent(chaptersLoader.snapshot(), currentState.value)) + content.value = ReaderContent(chaptersLoader.snapshot(), currentState.value) } } @@ -353,7 +351,7 @@ class ReaderViewModel @Inject constructor( loadingJob = launchLoadingJob(Dispatchers.Default) { prevJob?.join() chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext) - content.emitValue(ReaderContent(chaptersLoader.snapshot(), null)) + content.value = ReaderContent(chaptersLoader.snapshot(), null) } } @@ -367,46 +365,27 @@ class ReaderViewModel @Inject constructor( } } - private suspend fun detectReaderMode(manga: Manga, repo: MangaRepository): ReaderMode { - dataRepository.getReaderMode(manga.id)?.let { return it } - val defaultMode = settings.defaultReaderMode - if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) { - return defaultMode - } - val chapter = currentState.value?.chapterId?.let(chapters::get) - ?: manga.chapters?.randomOrNull() - ?: error("There are no chapters in this manga") - val pages = repo.getPages(chapter) - return runCatchingCancellable { - val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages) - if (isWebtoon) ReaderMode.WEBTOON else defaultMode - }.onSuccess { - dataRepository.saveReaderMode(manga, it) - }.onFailure { - it.printStackTraceDebug() - }.getOrDefault(defaultMode) - } - @WorkerThread private fun notifyStateChanged() { val state = getCurrentState() - val chapter = state?.chapterId?.let(chapters::get) + val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) } val newState = ReaderUiState( - mangaName = manga?.title, + mangaName = manga?.any?.title, + branch = chapter?.branch, chapterName = chapter?.name, chapterNumber = chapter?.number ?: 0, - chaptersTotal = manga?.getChapters(chapter?.branch)?.size ?: 0, + chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0, totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0, currentPage = state?.page ?: 0, isSliderEnabled = settings.isReaderSliderEnabled, percent = if (state != null) computePercent(state.chapterId, state.page) else PROGRESS_NONE, ) - uiState.postValue(newState) + uiState.value = newState } private fun computePercent(chapterId: Long, pageIndex: Int): Float { - val branch = chapters[chapterId]?.branch - val chapters = manga?.getChapters(branch) ?: return PROGRESS_NONE + val branch = chaptersLoader.peekChapter(chapterId)?.branch + val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE val chaptersCount = chapters.size val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val pagesCount = chaptersLoader.getPagesCount(chapterId) @@ -418,23 +397,3 @@ class ReaderViewModel @Inject constructor( return ppc * chapterIndex + ppc * pagePercent } } - -/** - * This function is not a member of the ReaderViewModel - * because it should work independently of the ViewModel's lifecycle. - */ -private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job { - return processLifecycleScope.launch(Dispatchers.Default) { - runCatchingCancellable { - addOrUpdate( - manga = manga, - chapterId = state.chapterId, - page = state.page, - scroll = state.scroll, - percent = percent, - ) - }.onFailure { - it.printStackTraceDebug() - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScrollTimer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt index 471ade52d..8bbe5f972 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt @@ -6,6 +6,7 @@ import android.content.res.Resources import android.os.Bundle import android.view.View import android.view.ViewGroup +import android.widget.CompoundButton import androidx.activity.viewModels import androidx.core.graphics.Insets import androidx.core.view.updateLayoutParams @@ -18,18 +19,21 @@ import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.Slider import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.decodeRegion +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.indicator +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.setChecked +import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.reader.domain.ReaderColorFilter -import org.koitharu.kotatsu.utils.ext.decodeRegion -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.indicator -import org.koitharu.kotatsu.utils.ext.setValueRounded import javax.inject.Inject import com.google.android.material.R as materialR @@ -37,7 +41,7 @@ import com.google.android.material.R as materialR class ColorFilterConfigActivity : BaseActivity(), Slider.OnChangeListener, - View.OnClickListener { + View.OnClickListener, CompoundButton.OnCheckedChangeListener { @Inject lateinit var coil: ImageLoader @@ -51,22 +55,23 @@ class ColorFilterConfigActivity : setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } - binding.sliderBrightness.addOnChangeListener(this) - binding.sliderContrast.addOnChangeListener(this) + viewBinding.sliderBrightness.addOnChangeListener(this) + viewBinding.sliderContrast.addOnChangeListener(this) val formatter = PercentLabelFormatter(resources) - binding.sliderContrast.setLabelFormatter(formatter) - binding.sliderBrightness.setLabelFormatter(formatter) - binding.buttonDone.setOnClickListener(this) - binding.buttonReset.setOnClickListener(this) + viewBinding.sliderContrast.setLabelFormatter(formatter) + viewBinding.sliderBrightness.setLabelFormatter(formatter) + viewBinding.switchInvert.setOnCheckedChangeListener(this) + viewBinding.buttonDone.setOnClickListener(this) + viewBinding.buttonReset.setOnClickListener(this) onBackPressedDispatcher.addCallback(ColorFilterConfigBackPressedDispatcher(this, viewModel)) viewModel.colorFilter.observe(this, this::onColorFilterChanged) viewModel.isLoading.observe(this, this::onLoadingChanged) - viewModel.preview.observe(this, this::onPreviewChanged) - viewModel.onDismiss.observe(this) { + viewModel.onDismiss.observeEvent(this) { finishAfterTransition() } + loadPreview(viewModel.preview) } override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { @@ -78,6 +83,10 @@ class ColorFilterConfigActivity : } } + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + viewModel.setInversion(isChecked) + } + override fun onClick(v: View) { when (v.id) { R.id.button_done -> viewModel.save() @@ -86,43 +95,44 @@ class ColorFilterConfigActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) - binding.scrollView.updatePadding( + viewBinding.scrollView.updatePadding( bottom = insets.bottom, ) - binding.toolbar.updateLayoutParams { + viewBinding.toolbar.updateLayoutParams { topMargin = insets.top } } private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) { - binding.sliderBrightness.setValueRounded(readerColorFilter?.brightness ?: 0f) - binding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f) - binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() + viewBinding.sliderBrightness.setValueRounded(readerColorFilter?.brightness ?: 0f) + viewBinding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f) + viewBinding.switchInvert.setChecked(readerColorFilter?.isInverted ?: false, false) + viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() } - private fun onPreviewChanged(preview: MangaPage?) { - if (preview == null) return + private fun loadPreview(page: MangaPage) { + val data: Any = page.preview?.takeUnless { it.isEmpty() } ?: page ImageRequest.Builder(this@ColorFilterConfigActivity) - .data(preview.url) + .data(data) .scale(Scale.FILL) .decodeRegion() - .tag(preview.source) - .indicator(listOf(binding.progressBefore, binding.progressAfter)) + .tag(page.source) + .indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter)) .error(R.drawable.ic_error_placeholder) - .size(ViewSizeResolver(binding.imageViewBefore)) + .size(ViewSizeResolver(viewBinding.imageViewBefore)) .allowRgb565(false) - .target(ShadowViewTarget(binding.imageViewBefore, binding.imageViewAfter)) + .target(DoubleViewTarget(viewBinding.imageViewBefore, viewBinding.imageViewAfter)) .enqueueWith(coil) } private fun onLoadingChanged(isLoading: Boolean) { - binding.sliderContrast.isEnabled = !isLoading - binding.sliderBrightness.isEnabled = !isLoading - binding.buttonDone.isEnabled = !isLoading + viewBinding.sliderContrast.isEnabled = !isLoading + viewBinding.sliderBrightness.isEnabled = !isLoading + viewBinding.buttonDone.isEnabled = !isLoading } private class PercentLabelFormatter(resources: Resources) : LabelFormatter { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt index a7d017c15..97527946c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigBackPressedDispatcher.kt @@ -5,6 +5,7 @@ import android.content.DialogInterface import androidx.activity.OnBackPressedCallback import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.call class ColorFilterConfigBackPressedDispatcher( private val context: Context, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt new file mode 100644 index 000000000..00ac1a23d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.reader.ui.colorfilter + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.reader.domain.ReaderColorFilter +import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA +import javax.inject.Inject + +@HiltViewModel +class ColorFilterConfigViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val mangaDataRepository: MangaDataRepository, +) : BaseViewModel() { + + private val manga = savedStateHandle.require(EXTRA_MANGA).manga + + private var initialColorFilter: ReaderColorFilter? = null + val colorFilter = MutableStateFlow(null) + val onDismiss = MutableEventFlow() + val preview = savedStateHandle.require(ColorFilterConfigActivity.EXTRA_PAGES).pages.first() + + val isChanged: Boolean + get() = colorFilter.value != initialColorFilter + + init { + launchLoadingJob { + initialColorFilter = mangaDataRepository.getColorFilter(manga.id) + colorFilter.value = initialColorFilter + } + } + + fun setBrightness(brightness: Float) { + val cf = colorFilter.value + colorFilter.value = ReaderColorFilter( + brightness = brightness, + contrast = cf?.contrast ?: 0f, + isInverted = cf?.isInverted ?: false, + ).takeUnless { it.isEmpty } + } + + fun setContrast(contrast: Float) { + val cf = colorFilter.value + colorFilter.value = ReaderColorFilter( + brightness = cf?.brightness ?: 0f, + contrast = contrast, + isInverted = cf?.isInverted ?: false, + ).takeUnless { it.isEmpty } + } + + fun setInversion(invert: Boolean) { + val cf = colorFilter.value + if (invert == cf?.isInverted) { + return + } + colorFilter.value = ReaderColorFilter( + brightness = cf?.brightness ?: 0f, + contrast = cf?.contrast ?: 0f, + isInverted = invert, + ).takeUnless { it.isEmpty } + } + + fun reset() { + colorFilter.value = null + } + + fun save() { + launchLoadingJob(Dispatchers.Default) { + mangaDataRepository.saveColorFilter(manga, colorFilter.value) + onDismiss.call(Unit) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ShadowViewTarget.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/DoubleViewTarget.kt similarity index 58% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ShadowViewTarget.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/DoubleViewTarget.kt index fcd05cbd3..28bf4f38c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ShadowViewTarget.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/DoubleViewTarget.kt @@ -4,15 +4,15 @@ import android.graphics.drawable.Drawable import android.widget.ImageView import coil.target.ImageViewTarget -class ShadowViewTarget( - view: ImageView, - private val shadowView: ImageView, -) : ImageViewTarget(view) { +class DoubleViewTarget( + primaryView: ImageView, + private val secondaryView: ImageView, +) : ImageViewTarget(primaryView) { override var drawable: Drawable? get() = super.drawable set(value) { super.drawable = value - shadowView.setImageDrawable(value?.constantState?.newDrawable()) + secondaryView.setImageDrawable(value?.constantState?.newDrawable()) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 51c55af04..a29154d6f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -18,28 +18,31 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode -import org.koitharu.kotatsu.core.prefs.observeAsLiveData +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ScreenOrientationHelper +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding import org.koitharu.kotatsu.reader.ui.PageSaveContract import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.utils.ScreenOrientationHelper -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -import org.koitharu.kotatsu.utils.ext.withArgs import javax.inject.Inject @AndroidEntryPoint -class ReaderConfigBottomSheet : - BaseBottomSheet(), +class ReaderConfigSheet : + BaseAdaptiveSheet(), ActivityResultCallback, View.OnClickListener, MaterialButtonToggleGroup.OnButtonCheckedListener, - Slider.OnChangeListener, CompoundButton.OnCheckedChangeListener { + Slider.OnChangeListener, + CompoundButton.OnCheckedChangeListener { private val viewModel by activityViewModels() private val savePageRequest = registerForActivityResult(PageSaveContract(), this) @@ -56,12 +59,18 @@ class ReaderConfigBottomSheet : ?: ReaderMode.STANDARD } - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetReaderConfigBinding { + override fun onCreateViewBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ): SheetReaderConfigBinding { return SheetReaderConfigBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated( + binding: SheetReaderConfigBinding, + savedInstanceState: Bundle?, + ) { + super.onViewBindingCreated(binding, savedInstanceState) observeScreenOrientation() binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED @@ -75,8 +84,8 @@ class ReaderConfigBottomSheet : binding.sliderTimer.addOnChangeListener(this) binding.switchScrollTimer.setOnCheckedChangeListener(this) - settings.observeAsLiveData( - context = lifecycleScope.coroutineContext + Dispatchers.Default, + settings.observeAsStateFlow( + scope = lifecycleScope + Dispatchers.Default, key = AppSettings.KEY_READER_AUTOSCROLL_SPEED, valueProducer = { readerAutoscrollSpeed }, ).observe(viewLifecycleOwner) { @@ -108,7 +117,7 @@ class ReaderConfigBottomSheet : R.id.button_color_filter -> { val page = viewModel.getCurrentPage() ?: return - val manga = viewModel.manga ?: return + val manga = viewModel.manga?.any ?: return startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) } } @@ -118,13 +127,17 @@ class ReaderConfigBottomSheet : when (buttonView.id) { R.id.switch_scroll_timer -> { findCallback()?.isAutoScrollEnabled = isChecked - binding.labelTimer.isVisible = isChecked - binding.sliderTimer.isVisible = isChecked + requireViewBinding().labelTimer.isVisible = isChecked + requireViewBinding().sliderTimer.isVisible = isChecked } } } - override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) { + override fun onButtonChecked( + group: MaterialButtonToggleGroup?, + checkedId: Int, + isChecked: Boolean, + ) { if (!isChecked) { return } @@ -157,7 +170,7 @@ class ReaderConfigBottomSheet : orientationHelper = helper helper.observeAutoOrientation() .onEach { - binding.buttonScreenRotate.isGone = it + requireViewBinding().buttonScreenRotate.isGone = it }.launchIn(viewLifecycleScope) } @@ -177,7 +190,7 @@ class ReaderConfigBottomSheet : private const val TAG = "ReaderConfigBottomSheet" private const val ARG_MODE = "mode" - fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigBottomSheet().withArgs(1) { + fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigSheet().withArgs(1) { putInt(ARG_MODE, mode.id) }.show(fm, TAG) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt index 03a6c49e9..c1b5f5f4b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt @@ -26,7 +26,7 @@ class ReaderSettings( get() = settings.zoomMode val colorFilter: ReaderColorFilter? - get() = colorFilterFlow.value + get() = colorFilterFlow.value?.takeUnless { it.isEmpty } val isPagesNumbersEnabled: Boolean get() = settings.isPagesNumbersEnabled diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt index 3ab378cec..7a289cbdf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt @@ -6,9 +6,9 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings -import org.koitharu.kotatsu.utils.ext.resetTransformations import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt similarity index 66% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt index 954c866c8..293928fb5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt @@ -1,14 +1,14 @@ package org.koitharu.kotatsu.reader.ui.pager import android.os.Bundle -import android.view.View import androidx.core.graphics.Insets import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding -import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.util.ext.getParcelableCompat +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderViewModel -import org.koitharu.kotatsu.utils.ext.getParcelableCompat private const val KEY_STATE = "state" @@ -17,9 +17,13 @@ abstract class BaseReaderFragment : BaseFragment() { protected val viewModel by activityViewModels() private var stateToSave: ReaderState? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + protected var readerAdapter: BaseReaderAdapter<*>? = null + private set + + override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) var restoredState = savedInstanceState?.getParcelableCompat(KEY_STATE) + readerAdapter = onCreateAdapter() viewModel.content.observe(viewLifecycleOwner) { onPagesChanged(it.pages, restoredState ?: it.state) @@ -34,6 +38,7 @@ abstract class BaseReaderFragment : BaseFragment() { override fun onDestroyView() { stateToSave = getCurrentState() + readerAdapter = null super.onDestroyView() } @@ -45,6 +50,10 @@ abstract class BaseReaderFragment : BaseFragment() { outState.putParcelable(KEY_STATE, stateToSave) } + protected fun requireAdapter() = checkNotNull(readerAdapter) { + "Adapter was not created or already destroyed" + } + override fun onWindowInsetsChanged(insets: Insets) = Unit abstract fun switchPageBy(delta: Int) @@ -55,5 +64,7 @@ abstract class BaseReaderFragment : BaseFragment() { abstract fun getCurrentState(): ReaderState? - protected abstract fun onPagesChanged(pages: List, pendingState: ReaderState?) + protected abstract fun onCreateAdapter(): BaseReaderAdapter<*> + + protected abstract suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/OnBoundsScrollListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index a517f630a..617e79839 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import java.io.IOException diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt similarity index 58% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt index e97192ecf..650c4439a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt @@ -1,7 +1,11 @@ package org.koitharu.kotatsu.reader.ui.pager +import android.content.Context +import org.koitharu.kotatsu.R + data class ReaderUiState( val mangaName: String?, + val branch: String?, val chapterName: String?, val chapterNumber: Int, val chaptersTotal: Int, @@ -14,4 +18,10 @@ data class ReaderUiState( fun isSliderAvailable(): Boolean { return isSliderEnabled && totalPages > 1 && currentPage < totalPages } + + fun resolveTitle(context: Context): String? = when { + mangaName == null -> null + branch == null -> mangaName + else -> context.getString(R.string.manga_branch_title_template, mangaName, branch) + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageAnimTransformer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt similarity index 57% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt index 5bf30b8a5..beed525cd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt @@ -1,15 +1,21 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.view.children +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.yield +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.doOnPageChanged +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.recyclerView +import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.ReaderState @@ -17,11 +23,6 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment -import org.koitharu.kotatsu.utils.ext.doOnPageChanged -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled -import org.koitharu.kotatsu.utils.ext.recyclerView -import org.koitharu.kotatsu.utils.ext.resetTransformations -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import javax.inject.Inject import kotlin.math.absoluteValue @@ -34,25 +35,15 @@ class ReversedReaderFragment : BaseReaderFragment @Inject lateinit var pageLoader: PageLoader - private var pagerAdapter: ReversedPagesAdapter? = null - - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentReaderStandardBinding.inflate(inflater, container, false) - @SuppressLint("NotifyDataSetChanged") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - pagerAdapter = ReversedPagesAdapter( - lifecycleOwner = viewLifecycleOwner, - loader = pageLoader, - settings = viewModel.readerSettings, - networkState = networkState, - exceptionResolver = exceptionResolver, - ) + override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) with(binding.pager) { - adapter = pagerAdapter + adapter = readerAdapter offscreenPageLimit = 2 doOnPageChanged(::notifyPageChanged) } @@ -69,19 +60,26 @@ class ReversedReaderFragment : BaseReaderFragment } override fun onDestroyView() { - pagerAdapter = null - binding.pager.adapter = null + requireViewBinding().pager.adapter = null super.onDestroyView() } + override fun onCreateAdapter() = ReversedPagesAdapter( + lifecycleOwner = viewLifecycleOwner, + loader = pageLoader, + settings = viewModel.readerSettings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) + override fun switchPageBy(delta: Int) { - with(binding.pager) { + with(requireViewBinding().pager) { setCurrentItem(currentItem - delta, context.isAnimationsEnabled) } } override fun switchPageTo(position: Int, smooth: Boolean) { - with(binding.pager) { + with(requireViewBinding().pager) { setCurrentItem( reversed(position), smooth && context.isAnimationsEnabled && (currentItem - position).absoluteValue < PagerReaderFragment.SMOOTH_SCROLL_LIMIT, @@ -89,28 +87,30 @@ class ReversedReaderFragment : BaseReaderFragment } } - override fun onPagesChanged(pages: List, pendingState: ReaderState?) { + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = coroutineScope { val reversedPages = pages.asReversed() - viewLifecycleScope.launch { - val items = async { - pagerAdapter?.setItems(reversedPages) + val items = async { + requireAdapter().setItems(reversedPages) + yield() + } + if (pendingState != null) { + val position = reversedPages.indexOfLast { + it.chapterId == pendingState.chapterId && it.index == pendingState.page } - if (pendingState != null) { - val position = reversedPages.indexOfLast { - it.chapterId == pendingState.chapterId && it.index == pendingState.page - } - items.await() ?: return@launch - if (position != -1) { - binding.pager.setCurrentItem(position, false) - notifyPageChanged(position) - } + items.await() + if (position != -1) { + requireViewBinding().pager.setCurrentItem(position, false) + notifyPageChanged(position) } else { - items.await() + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() } + } else { + items.await() } } - override fun getCurrentState(): ReaderState? = bindingOrNull()?.run { + override fun getCurrentState(): ReaderState? = viewBinding?.run { val adapter = pager.adapter as? BaseReaderAdapter<*> val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null ReaderState( @@ -125,6 +125,6 @@ class ReversedReaderFragment : BaseReaderFragment } private fun reversed(position: Int): Int { - return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0) + return ((readerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageAnimTransformer.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index 6d8076baf..2cacd4eea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -12,12 +12,14 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.ifZero +import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.utils.ext.* open class PageHolder( owner: LifecycleOwner, @@ -31,7 +33,7 @@ open class PageHolder( init { binding.ssiv.bindToLifecycle(owner) - binding.ssiv.isEagerLoadingEnabled = !isLowRamDevice(context) + binding.ssiv.isEagerLoadingEnabled = !context.isLowRamDevice() binding.ssiv.addOnImageEventListener(delegate) @Suppress("LeakingThis") bindingInfo.buttonRetry.setOnClickListener(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerPaginationListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt similarity index 57% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt index 75462cfb0..a9c656113 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt @@ -1,26 +1,27 @@ package org.koitharu.kotatsu.reader.ui.pager.standard -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.view.children +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.yield +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.doOnPageChanged +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.recyclerView +import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.utils.ext.doOnPageChanged -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled -import org.koitharu.kotatsu.utils.ext.recyclerView -import org.koitharu.kotatsu.utils.ext.resetTransformations -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import javax.inject.Inject import kotlin.math.absoluteValue @@ -33,25 +34,15 @@ class PagerReaderFragment : BaseReaderFragment() @Inject lateinit var pageLoader: PageLoader - private var pagesAdapter: PagesAdapter? = null - - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentReaderStandardBinding.inflate(inflater, container, false) - @SuppressLint("NotifyDataSetChanged") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - pagesAdapter = PagesAdapter( - lifecycleOwner = viewLifecycleOwner, - loader = pageLoader, - settings = viewModel.readerSettings, - networkState = networkState, - exceptionResolver = exceptionResolver, - ) + override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) with(binding.pager) { - adapter = pagesAdapter + adapter = readerAdapter offscreenPageLimit = 2 doOnPageChanged(::notifyPageChanged) } @@ -68,39 +59,48 @@ class PagerReaderFragment : BaseReaderFragment() } override fun onDestroyView() { - pagesAdapter = null - binding.pager.adapter = null + requireViewBinding().pager.adapter = null super.onDestroyView() } - override fun onPagesChanged(pages: List, pendingState: ReaderState?) { - viewLifecycleScope.launch { - val items = async { - pagesAdapter?.setItems(pages) + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = coroutineScope { + val items = async { + requireAdapter().setItems(pages) + yield() + } + if (pendingState != null) { + val position = pages.indexOfFirst { + it.chapterId == pendingState.chapterId && it.index == pendingState.page } - if (pendingState != null) { - val position = pages.indexOfFirst { - it.chapterId == pendingState.chapterId && it.index == pendingState.page - } - items.await() ?: return@launch - if (position != -1) { - binding.pager.setCurrentItem(position, false) - notifyPageChanged(position) - } + items.await() + if (position != -1) { + requireViewBinding().pager.setCurrentItem(position, false) + notifyPageChanged(position) } else { - items.await() + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() } + } else { + items.await() } } + override fun onCreateAdapter() = PagesAdapter( + lifecycleOwner = viewLifecycleOwner, + loader = pageLoader, + settings = viewModel.readerSettings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) + override fun switchPageBy(delta: Int) { - with(binding.pager) { + with(requireViewBinding().pager) { setCurrentItem(currentItem + delta, context.isAnimationsEnabled) } } override fun switchPageTo(position: Int, smooth: Boolean) { - with(binding.pager) { + with(requireViewBinding().pager) { setCurrentItem( position, smooth && context.isAnimationsEnabled && (currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT, @@ -108,7 +108,7 @@ class PagerReaderFragment : BaseReaderFragment() } } - override fun getCurrentState(): ReaderState? = bindingOrNull()?.run { + override fun getCurrentState(): ReaderState? = viewBinding?.run { val adapter = pager.adapter as? BaseReaderAdapter<*> val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null ReaderState( diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/ListPaginationListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonFrameLayout.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index 1301a96b0..f5a936ad6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -10,14 +10,14 @@ import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.GoneOnInvisibleListener +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.utils.GoneOnInvisibleListener -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.ifZero class WebtoonHolder( owner: LifecycleOwner, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt index 55aa6540a..d27a05669 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonImageView.kt @@ -5,8 +5,8 @@ import android.graphics.PointF import android.util.AttributeSet import androidx.recyclerview.widget.RecyclerView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import org.koitharu.kotatsu.core.util.ext.parents import org.koitharu.kotatsu.parsers.util.toIntUp -import org.koitharu.kotatsu.utils.ext.parents private const val SCROLL_UNKNOWN = -1 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonLayoutManager.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt similarity index 56% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index e836c7fd0..78836411a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -2,23 +2,25 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.yield +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition +import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.ReaderPage -import org.koitharu.kotatsu.utils.ext.findCenterViewPosition -import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition -import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import javax.inject.Inject @AndroidEntryPoint @@ -31,25 +33,17 @@ class WebtoonReaderFragment : BaseReaderFragment() lateinit var pageLoader: PageLoader private val scrollInterpolator = AccelerateDecelerateInterpolator() - private var webtoonAdapter: WebtoonAdapter? = null - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentReaderWebtoonBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - webtoonAdapter = WebtoonAdapter( - lifecycleOwner = viewLifecycleOwner, - loader = pageLoader, - settings = viewModel.readerSettings, - networkState = networkState, - exceptionResolver = exceptionResolver, - ) + override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) with(binding.recyclerView) { setHasFixedSize(true) - adapter = webtoonAdapter + adapter = readerAdapter addOnPageScrollListener(PageScrollListener()) } @@ -59,36 +53,47 @@ class WebtoonReaderFragment : BaseReaderFragment() } override fun onDestroyView() { - webtoonAdapter = null - binding.recyclerView.adapter = null + requireViewBinding().recyclerView.adapter = null super.onDestroyView() } - override fun onPagesChanged(pages: List, pendingState: ReaderState?) { - viewLifecycleScope.launch { - val setItems = async { webtoonAdapter?.setItems(pages) } - if (pendingState != null) { - val position = pages.indexOfFirst { - it.chapterId == pendingState.chapterId && it.index == pendingState.page - } - setItems.await() ?: return@launch - if (position != -1) { - with(binding.recyclerView) { - firstVisibleItemPosition = position - post { - (findViewHolderForAdapterPosition(position) as? WebtoonHolder) - ?.restoreScroll(pendingState.scroll) - } + override fun onCreateAdapter() = WebtoonAdapter( + lifecycleOwner = viewLifecycleOwner, + loader = pageLoader, + settings = viewModel.readerSettings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) + + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = coroutineScope { + val setItems = async { + requireAdapter().setItems(pages) + yield() + } + if (pendingState != null) { + val position = pages.indexOfFirst { + it.chapterId == pendingState.chapterId && it.index == pendingState.page + } + setItems.await() + if (position != -1) { + with(requireViewBinding().recyclerView) { + firstVisibleItemPosition = position + post { + (findViewHolderForAdapterPosition(position) as? WebtoonHolder) + ?.restoreScroll(pendingState.scroll) } - notifyPageChanged(position) } + notifyPageChanged(position) } else { - setItems.await() + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() } + } else { + setItems.await() } } - override fun getCurrentState(): ReaderState? = bindingOrNull()?.run { + override fun getCurrentState(): ReaderState? = viewBinding?.run { val currentItem = recyclerView.findCenterViewPosition() val adapter = recyclerView.adapter as? BaseReaderAdapter<*> val page = adapter?.getItemOrNull(currentItem) ?: return@run null @@ -105,7 +110,7 @@ class WebtoonReaderFragment : BaseReaderFragment() } override fun switchPageBy(delta: Int) { - with(binding.recyclerView) { + with(requireViewBinding().recyclerView) { if (context.isAnimationsEnabled) { smoothScrollBy(0, (height * 0.9).toInt() * delta, scrollInterpolator) } else { @@ -115,11 +120,11 @@ class WebtoonReaderFragment : BaseReaderFragment() } override fun switchPageTo(position: Int, smooth: Boolean) { - binding.recyclerView.firstVisibleItemPosition = position + requireViewBinding().recyclerView.firstVisibleItemPosition = position } override fun scrollBy(delta: Int): Boolean { - binding.recyclerView.nestedScrollBy(0, delta) + requireViewBinding().recyclerView.nestedScrollBy(0, delta) return true } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt index aa28971af..2a0acaee3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt @@ -4,8 +4,8 @@ import android.content.Context import android.util.AttributeSet import androidx.core.view.ViewCompat.TYPE_TOUCH import androidx.recyclerview.widget.RecyclerView -import org.koitharu.kotatsu.utils.ext.findCenterViewPosition -import java.util.* +import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition +import java.util.LinkedList class WebtoonRecyclerView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -60,6 +60,7 @@ class WebtoonRecyclerView @JvmOverloads constructor( } return consumedByChild } + dy < 0 -> { val child = getChildAt(childCount - 1) as WebtoonFrameLayout var consumedByChild = child.dispatchVerticalScroll(dy) @@ -113,4 +114,4 @@ class WebtoonRecyclerView @JvmOverloads constructor( open fun onPageChanged(recyclerView: WebtoonRecyclerView, index: Int) = Unit } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonScalingFrame.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt new file mode 100644 index 000000000..393f6398d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt @@ -0,0 +1,118 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails + +import android.content.Context +import androidx.core.net.toUri +import coil.ImageLoader +import coil.decode.DataSource +import coil.decode.ImageSource +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.fetch.SourceResult +import coil.request.Options +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import okhttp3.OkHttpClient +import okio.Path.Companion.toOkioPath +import okio.buffer +import okio.source +import org.koitharu.kotatsu.core.network.ImageProxyInterceptor +import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.local.data.CbzFilter +import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.local.data.util.withExtraCloseable +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.util.mimeType +import org.koitharu.kotatsu.reader.domain.PageLoader +import java.util.zip.ZipFile +import javax.inject.Inject + +class MangaPageFetcher( + private val context: Context, + private val okHttpClient: OkHttpClient, + private val pagesCache: PagesCache, + private val options: Options, + private val page: MangaPage, + private val mangaRepositoryFactory: MangaRepository.Factory, + private val imageProxyInterceptor: ImageProxyInterceptor, +) : Fetcher { + + override suspend fun fetch(): FetchResult { + val repo = mangaRepositoryFactory.create(page.source) + val pageUrl = repo.getPageUrl(page) + pagesCache.get(pageUrl)?.let { file -> + return SourceResult( + source = ImageSource( + file = file.toOkioPath(), + metadata = MangaPageMetadata(page), + ), + mimeType = null, + dataSource = DataSource.DISK, + ) + } + return loadPage(pageUrl) + } + + private suspend fun loadPage(pageUrl: String): SourceResult { + val uri = pageUrl.toUri() + return if (CbzFilter.isUriSupported(uri)) { + val zip = runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart) } + val entry = runInterruptible(Dispatchers.IO) { zip.getEntry(uri.fragment) } + return SourceResult( + source = ImageSource( + source = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer(), + context = context, + metadata = MangaPageMetadata(page), + ), + mimeType = null, + dataSource = DataSource.DISK, + ) + } else { + val request = PageLoader.createPageRequest(page, pageUrl) + imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> + check(response.isSuccessful) { + "Invalid response: ${response.code} ${response.message} at $pageUrl" + } + val body = checkNotNull(response.body) { + "Null response" + } + val mimeType = response.mimeType + val file = body.use { + pagesCache.put(pageUrl, it.source()) + } + SourceResult( + source = ImageSource( + file = file.toOkioPath(), + metadata = MangaPageMetadata(page), + ), + mimeType = mimeType, + dataSource = DataSource.NETWORK, + ) + } + } + } + + class Factory @Inject constructor( + @ApplicationContext private val context: Context, + @MangaHttpClient private val okHttpClient: OkHttpClient, + private val pagesCache: PagesCache, + private val mangaRepositoryFactory: MangaRepository.Factory, + private val imageProxyInterceptor: ImageProxyInterceptor, + ) : Fetcher.Factory { + + override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher { + return MangaPageFetcher( + okHttpClient = okHttpClient, + pagesCache = pagesCache, + options = options, + page = data, + context = context, + mangaRepositoryFactory = mangaRepositoryFactory, + imageProxyInterceptor = imageProxyInterceptor, + ) + } + } + + class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt new file mode 100644 index 000000000..3b6281c64 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/OnPageSelectListener.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails + +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage + +fun interface OnPageSelectListener { + + fun onPageSelected(page: ReaderPage) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt new file mode 100644 index 000000000..f413b9615 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PageThumbnail.kt @@ -0,0 +1,34 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails + +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage + +class PageThumbnail( + val isCurrent: Boolean, + val repository: MangaRepository, + val page: ReaderPage, +) : ListModel { + + val number + get() = page.index + 1 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PageThumbnail + + if (isCurrent != other.isCurrent) return false + if (repository != other.repository) return false + return page == other.page + } + + override fun hashCode(): Int { + var result = isCurrent.hashCode() + result = 31 * result + repository.hashCode() + result = 31 * result + page.hashCode() + return result + } + +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt new file mode 100644 index 000000000..2886c1a43 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -0,0 +1,200 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.ImageLoader +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior +import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.plus +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.SheetPagesBinding +import org.koitharu.kotatsu.list.ui.MangaListSpanResolver +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder +import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter +import javax.inject.Inject +import kotlin.math.roundToInt + +@AndroidEntryPoint +class PagesThumbnailsSheet : + BaseAdaptiveSheet(), + AdaptiveSheetCallback, + OnListItemClickListener { + + private val viewModel by viewModels() + + @Inject + lateinit var coil: ImageLoader + + @Inject + lateinit var settings: AppSettings + + private var thumbnailsAdapter: PageThumbnailAdapter? = null + private var spanResolver: MangaListSpanResolver? = null + private var scrollListener: ScrollListener? = null + + private val spanSizeLookup = SpanSizeLookup() + private val listCommitCallback = Runnable { + spanSizeLookup.invalidateCache() + } + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding { + return SheetPagesBinding.inflate(inflater, container, false) + } + + override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + addSheetCallback(this) + spanResolver = MangaListSpanResolver(binding.root.resources) + thumbnailsAdapter = PageThumbnailAdapter( + coil = coil, + lifecycleOwner = viewLifecycleOwner, + clickListener = this@PagesThumbnailsSheet, + ) + with(binding.recyclerView) { + addItemDecoration( + SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), + ) + adapter = thumbnailsAdapter + addOnLayoutChangeListener(spanResolver) + spanResolver?.setGridSize(settings.gridSize / 100f, this) + addOnScrollListener(ScrollListener().also { scrollListener = it }) + (layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup + } + viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) + viewModel.branch.observe(viewLifecycleOwner, ::updateTitle) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + } + + override fun onDestroyView() { + spanResolver = null + scrollListener = null + thumbnailsAdapter = null + spanSizeLookup.invalidateCache() + super.onDestroyView() + } + + override fun onItemClick(item: PageThumbnail, view: View) { + val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener) + if (listener != null) { + listener.onPageSelected(item.page) + } else { + val state = ReaderState(item.page.chapterId, item.page.index, 0) + val intent = IntentBuilder(view.context).manga(viewModel.manga).state(state).build() + startActivity(intent, scaleUpActivityOptionsOf(view)) + } + dismiss() + } + + override fun onStateChanged(sheet: View, newState: Int) { + viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED + } + + private fun updateTitle(branch: String?) { + val mangaName = viewModel.manga.title + viewBinding?.headerBar?.title = if (branch != null) { + getString(R.string.manga_branch_title_template, mangaName, branch) + } else { + mangaName + } + } + + private fun onThumbnailsChanged(list: List) { + val adapter = thumbnailsAdapter ?: return + if (adapter.itemCount == 0) { + var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent } + if (position > 0) { + val spanCount = spanResolver?.spanCount ?: 0 + val offset = if (position > spanCount + 1) { + (resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt() + } else { + position = 0 + 0 + } + val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset) + adapter.setItems(list, listCommitCallback + scrollCallback) + } else { + adapter.setItems(list, listCommitCallback) + } + } else { + adapter.setItems(list, listCommitCallback) + } + } + + private inner class ScrollListener : BoundsScrollListener(3, 3) { + + override fun onScrolledToStart(recyclerView: RecyclerView) { + viewModel.loadPrevChapter() + } + + override fun onScrolledToEnd(recyclerView: RecyclerView) { + viewModel.loadNextChapter() + } + + override fun onPostScrolled(recyclerView: RecyclerView, firstVisibleItemPosition: Int, visibleItemCount: Int) { + super.onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount) + if (firstVisibleItemPosition > offsetTop) { + viewModel.allowLoadAbove() + } + } + } + + private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() { + + init { + isSpanIndexCacheEnabled = true + isSpanGroupIndexCacheEnabled = true + } + + override fun getSpanSize(position: Int): Int { + val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1 + return when (thumbnailsAdapter?.getItemViewType(position)) { + PageThumbnailAdapter.ITEM_TYPE_THUMBNAIL -> 1 + else -> total + } + } + + fun invalidateCache() { + invalidateSpanGroupIndexCache() + invalidateSpanIndexCache() + } + } + + companion object { + + const val ARG_MANGA = "manga" + const val ARG_CURRENT_PAGE = "current" + const val ARG_CHAPTER_ID = "chapter_id" + + private const val TAG = "PagesThumbnailsSheet" + + fun show(fm: FragmentManager, manga: Manga, chapterId: Long, currentPage: Int = -1) { + PagesThumbnailsSheet().withArgs(3) { + putParcelable(ARG_MANGA, ParcelableManga(manga, true)) + putLong(ARG_CHAPTER_ID, chapterId) + putInt(ARG_CURRENT_PAGE, currentPage) + }.show(fm, TAG) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt new file mode 100644 index 000000000..7fee3bf49 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt @@ -0,0 +1,114 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.reader.domain.ChaptersLoader +import javax.inject.Inject + +@HiltViewModel +class PagesThumbnailsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + mangaRepositoryFactory: MangaRepository.Factory, + private val chaptersLoader: ChaptersLoader, + private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, +) : BaseViewModel() { + + private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1 + private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L + val manga = savedStateHandle.require(PagesThumbnailsSheet.ARG_MANGA).manga + + private val repository = mangaRepositoryFactory.create(manga.source) + private val mangaDetails = SuspendLazy { + doubleMangaLoadUseCase(manga).let { + val b = manga.chapters?.find { ch -> ch.id == initialChapterId }?.branch + branch.value = b + it.filterChapters(b) + } + } + private var loadingJob: Job? = null + private var loadingPrevJob: Job? = null + private var loadingNextJob: Job? = null + private var isLoadAboveAllowed = false + + val thumbnails = MutableStateFlow>(emptyList()) + val branch = MutableStateFlow(null) + + init { + loadingJob = launchJob(Dispatchers.Default) { + chaptersLoader.init(mangaDetails.get()) + chaptersLoader.loadSingleChapter(initialChapterId) + updateList() + } + } + + fun allowLoadAbove() { + if (!isLoadAboveAllowed) { + loadingJob = launchJob(Dispatchers.Default) { + isLoadAboveAllowed = true + updateList() + } + } + } + + fun loadPrevChapter() { + if (!isLoadAboveAllowed || loadingJob?.isActive == true || loadingPrevJob?.isActive == true) { + return + } + loadingPrevJob = loadPrevNextChapter(isNext = false) + } + + fun loadNextChapter() { + if (loadingJob?.isActive == true || loadingNextJob?.isActive == true) { + return + } + loadingNextJob = loadPrevNextChapter(isNext = true) + } + + private fun loadPrevNextChapter(isNext: Boolean): Job = launchLoadingJob(Dispatchers.Default) { + val currentId = (if (isNext) chaptersLoader.last() else chaptersLoader.first()).chapterId + chaptersLoader.loadPrevNextChapter(mangaDetails.get(), currentId, isNext) + updateList() + } + + private suspend fun updateList() { + val snapshot = chaptersLoader.snapshot() + val mangaChapters = mangaDetails.tryGet().getOrNull()?.chapters.orEmpty() + val hasPrevChapter = isLoadAboveAllowed && snapshot.firstOrNull()?.chapterId != mangaChapters.firstOrNull()?.id + val hasNextChapter = snapshot.lastOrNull()?.chapterId != mangaChapters.lastOrNull()?.id + val pages = buildList(snapshot.size + chaptersLoader.size + 2) { + if (hasPrevChapter) { + add(LoadingFooter(-1)) + } + var previousChapterId = 0L + for (page in snapshot) { + if (page.chapterId != previousChapterId) { + chaptersLoader.peekChapter(page.chapterId)?.let { + add(ListHeader(it.name, 0, null)) + } + previousChapterId = page.chapterId + } + this += PageThumbnail( + isCurrent = page.chapterId == initialChapterId && page.index == currentPageIndex, + repository = repository, + page = page, + ) + } + if (hasNextChapter) { + add(LoadingFooter(1)) + } + } + thumbnails.value = pages + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt new file mode 100644 index 000000000..62ab44cb9 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -0,0 +1,63 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails.adapter + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.size.Scale +import coil.size.Size +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.decodeRegion +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.setTextColorAttr +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.databinding.ItemPageThumbBinding +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail +import com.google.android.material.R as materialR + +fun pageThumbnailAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }, +) { + + val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) + val thumbSize = Size( + width = gridWidth, + height = (gridWidth / 13f * 18f).toInt(), + ) + + val clickListenerAdapter = AdapterDelegateClickListenerAdapter(this, clickListener) + binding.root.setOnClickListener(clickListenerAdapter) + binding.root.setOnLongClickListener(clickListenerAdapter) + + bind { + val data: Any = item.page.preview?.takeUnless { it.isEmpty() } ?: item.page.toMangaPage() + binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run { + placeholder(R.drawable.ic_placeholder) + fallback(R.drawable.ic_placeholder) + error(R.drawable.ic_error_placeholder) + size(thumbSize) + scale(Scale.FILL) + allowRgb565(true) + decodeRegion(0) + source(item.page.source) + enqueueWith(coil) + } + with(binding.textViewNumber) { + setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty) + setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary) + text = (item.number).toString() + } + } + + onViewRecycled { + binding.imageViewThumb.disposeImageRequest() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt new file mode 100644 index 000000000..1b197fdb6 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt @@ -0,0 +1,73 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails.adapter + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD +import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail + +class PageThumbnailAdapter( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()), FastScroller.SectionIndexer { + + init { + delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener)) + .addDelegate(ITEM_TYPE_HEADER, listHeaderAD(null)) + .addDelegate(ITEM_LOADING, loadingFooterAD()) + } + + override fun getSectionText(context: Context, position: Int): CharSequence? { + val list = items + for (i in (0..position).reversed()) { + val item = list.getOrNull(i) ?: continue + if (item is ListHeader) { + return item.getText(context) + } + } + return null + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return when { + oldItem is PageThumbnail && newItem is PageThumbnail -> { + oldItem.page == newItem.page + } + + oldItem is ListHeader && newItem is ListHeader -> { + oldItem.textRes == newItem.textRes && + oldItem.text == newItem.text && + oldItem.dateTimeAgo == newItem.dateTimeAgo + } + + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + + else -> oldItem.javaClass == newItem.javaClass + } + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return oldItem == newItem + } + } + + companion object { + + const val ITEM_TYPE_THUMBNAIL = 0 + const val ITEM_TYPE_HEADER = 1 + const val ITEM_LOADING = 2 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/TargetScrollObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/TargetScrollObserver.kt new file mode 100644 index 000000000..bc27e4a01 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/TargetScrollObserver.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails.adapter + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail + +class TargetScrollObserver( + private val recyclerView: RecyclerView, +) : RecyclerView.AdapterDataObserver() { + + private var isScrollToCurrentPending = true + + private val layoutManager: LinearLayoutManager + get() = recyclerView.layoutManager as LinearLayoutManager + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (isScrollToCurrentPending) { + postScroll() + } + } + + private fun postScroll() { + recyclerView.post { + scrollToTarget() + } + } + + private fun scrollToTarget() { + val adapter = recyclerView.adapter ?: return + if (recyclerView.computeVerticalScrollRange() == 0) { + return + } + val snapshot = (adapter as? AsyncListDifferDelegationAdapter<*>)?.items ?: return + val target = snapshot.indexOfFirst { it is PageThumbnail && it.isCurrent } + if (target in snapshot.indices) { + layoutManager.scrollToPositionWithOffset(target, 0) + isScrollToCurrentPending = false + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index e597c90a5..fed94e8c9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -11,23 +11,24 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.FragmentListBinding +import org.koitharu.kotatsu.filter.ui.FilterSheetFragment import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint class RemoteListFragment : MangaListFragment() { - public override val viewModel by viewModels() + override val viewModel by viewModels() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(RemoteListMenuProvider()) } @@ -41,7 +42,7 @@ class RemoteListFragment : MangaListFragment() { } override fun onFilterClick(view: View?) { - FilterBottomSheet.show(childFragmentManager) + FilterSheetFragment.show(childFragmentManager) } override fun onEmptyActionClick() { diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt similarity index 51% rename from app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 0f2f18880..b89e4788c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.remotelist.ui -import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -9,26 +8,26 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter -import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.filter.ui.FilterCoordinator +import org.koitharu.kotatsu.filter.ui.FilterOwner +import org.koitharu.kotatsu.filter.ui.model.FilterState +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator -import org.koitharu.kotatsu.list.ui.filter.FilterItem -import org.koitharu.kotatsu.list.ui.filter.FilterState -import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.ListHeader2 import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorFooter @@ -37,60 +36,49 @@ import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.search.domain.MangaSearchRepository -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.require -import java.util.LinkedList +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject private const val FILTER_MIN_INTERVAL = 250L @HiltViewModel -class RemoteListViewModel @Inject constructor( +open class RemoteListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, mangaRepositoryFactory: MangaRepository.Factory, - private val searchRepository: MangaSearchRepository, + private val filter: FilterCoordinator, settings: AppSettings, - dataRepository: MangaDataRepository, - private val tagHighlighter: MangaTagHighlighter, + listExtraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, -) : MangaListViewModel(settings, downloadScheduler), OnFilterChangedListener { +) : MangaListViewModel(settings, downloadScheduler), FilterOwner by filter { val source = savedStateHandle.require(RemoteListFragment.ARG_SOURCE) - private val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository - private val filter = FilterCoordinator(repository, dataRepository, viewModelScope) + private val repository = mangaRepositoryFactory.create(source) private val mangaList = MutableStateFlow?>(null) private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null) private var loadingJob: Job? = null - val filterItems: LiveData> - get() = filter.items - override val content = combine( mangaList, - listModeFlow, - createHeaderFlow(), + listMode, listError, hasNextPage, - ) { list, mode, header, error, hasNext -> + ) { list, mode, error, hasNext -> buildList(list?.size?.plus(2) ?: 2) { - add(header) when { list.isNullOrEmpty() && error != null -> add(error.toErrorState(canRetry = true)) list == null -> add(LoadingState) - list.isEmpty() -> add(createEmptyState(header.hasSelectedTags)) + list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags)) else -> { - list.toUi(this, mode, tagHighlighter) + list.toUi(this, mode, listExtraProvider) when { error != null -> add(error.toErrorFooter()) - hasNext -> add(LoadingFooter) + hasNext -> add(LoadingFooter()) } } } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { filter.observeState() @@ -113,37 +101,23 @@ class RemoteListViewModel @Inject constructor( loadList(filter.snapshot(), append = !mangaList.value.isNullOrEmpty()) } - override fun onSortItemClick(item: FilterItem.Sort) { - filter.onSortItemClick(item) - } - - override fun onTagItemClick(item: FilterItem.Tag) { - filter.onTagItemClick(item) - } - fun loadNextPage() { if (hasNextPage.value && listError.value == null) { loadList(filter.snapshot(), append = true) } } - fun filterSearch(query: String) = filter.performSearch(query) - fun resetFilter() = filter.reset() override fun onUpdateFilter(tags: Set) { applyFilter(tags) } - fun applyFilter(tags: Set) { - filter.setTags(tags) - } - - private fun loadList(filterState: FilterState, append: Boolean) { - if (loadingJob?.isActive == true) { - return + protected fun loadList(filterState: FilterState, append: Boolean): Job { + loadingJob?.let { + if (it.isActive) return it } - loadingJob = launchLoadingJob(Dispatchers.Default) { + return launchLoadingJob(Dispatchers.Default) { try { listError.value = null val list = repository.getList( @@ -163,64 +137,16 @@ class RemoteListViewModel @Inject constructor( e.printStackTraceDebug() listError.value = e if (!mangaList.value.isNullOrEmpty()) { - errorEvent.emitCall(e) + errorEvent.call(e) } } - } + }.also { loadingJob = it } } - private fun createEmptyState(canResetFilter: Boolean) = EmptyState( + protected open fun createEmptyState(canResetFilter: Boolean) = EmptyState( icon = R.drawable.ic_empty_common, textPrimary = R.string.nothing_found, textSecondary = 0, actionStringRes = if (canResetFilter) R.string.reset_filter else 0, ) - - private fun createHeaderFlow() = combine( - filter.observeState(), - filter.observeAvailableTags(), - ) { state, available -> - val chips = createChipsList(state, available.orEmpty()) - ListHeader2(chips, state.sortOrder, state.tags.isNotEmpty()) - } - - private suspend fun createChipsList( - filterState: FilterState, - availableTags: Set, - ): List { - val selectedTags = filterState.tags.toMutableSet() - var tags = searchRepository.getTagsSuggestion("", 6, repository.source) - if (tags.isEmpty()) { - tags = availableTags.take(6) - } - if (tags.isEmpty() && selectedTags.isEmpty()) { - return emptyList() - } - val result = LinkedList() - for (tag in tags) { - val model = ChipsView.ChipModel( - tint = 0, - title = tag.title, - isCheckable = true, - isChecked = selectedTags.remove(tag), - data = tag, - ) - if (model.isChecked) { - result.addFirst(model) - } else { - result.addLast(model) - } - } - for (tag in selectedTags) { - val model = ChipsView.ChipModel( - tint = 0, - title = tag.title, - isCheckable = true, - isChecked = true, - data = tag, - ) - result.addFirst(model) - } - return result - } } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt index f4300792b..f45ce2123 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt @@ -8,12 +8,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dagger.multibindings.ElementsIntoSet import okhttp3.OkHttpClient -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.network.CurlLoggingInterceptor +import org.koitharu.kotatsu.core.network.BaseHttpClient import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor -import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler @@ -21,11 +18,9 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor -import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor -import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.scrobbling.shikimori.domain.ShikimoriScrobbler import javax.inject.Singleton @@ -35,57 +30,39 @@ object ScrobblingModule { @Provides @Singleton - fun provideShikimoriRepository( - @ApplicationContext context: Context, - @ScrobblerType(ScrobblerService.SHIKIMORI) storage: ScrobblerStorage, - database: MangaDatabase, + @ScrobblerType(ScrobblerService.SHIKIMORI) + fun provideShikimoriHttpClient( + @BaseHttpClient baseHttpClient: OkHttpClient, authenticator: ShikimoriAuthenticator, - ): ShikimoriRepository { - val okHttp = OkHttpClient.Builder().apply { - authenticator(authenticator) - addInterceptor(ShikimoriInterceptor(storage)) - if (BuildConfig.DEBUG) { - addInterceptor(CurlLoggingInterceptor()) - } - }.build() - return ShikimoriRepository(context, okHttp, storage, database) - } + @ScrobblerType(ScrobblerService.SHIKIMORI) storage: ScrobblerStorage, + ): OkHttpClient = baseHttpClient.newBuilder().apply { + authenticator(authenticator) + addInterceptor(ShikimoriInterceptor(storage)) + }.build() @Provides @Singleton - fun provideMALRepository( - @ApplicationContext context: Context, - @ScrobblerType(ScrobblerService.MAL) storage: ScrobblerStorage, - database: MangaDatabase, + @ScrobblerType(ScrobblerService.MAL) + fun provideMALHttpClient( + @BaseHttpClient baseHttpClient: OkHttpClient, authenticator: MALAuthenticator, - ): MALRepository { - val okHttp = OkHttpClient.Builder().apply { - authenticator(authenticator) - addInterceptor(MALInterceptor(storage)) - if (BuildConfig.DEBUG) { - addInterceptor(CurlLoggingInterceptor()) - } - }.build() - return MALRepository(context, okHttp, storage, database) - } + @ScrobblerType(ScrobblerService.MAL) storage: ScrobblerStorage, + ): OkHttpClient = baseHttpClient.newBuilder().apply { + authenticator(authenticator) + addInterceptor(MALInterceptor(storage)) + }.build() @Provides @Singleton - fun provideAniListRepository( - @ApplicationContext context: Context, - @ScrobblerType(ScrobblerService.ANILIST) storage: ScrobblerStorage, - database: MangaDatabase, + @ScrobblerType(ScrobblerService.ANILIST) + fun provideAniListHttpClient( + @BaseHttpClient baseHttpClient: OkHttpClient, authenticator: AniListAuthenticator, - ): AniListRepository { - val okHttp = OkHttpClient.Builder().apply { - authenticator(authenticator) - addInterceptor(AniListInterceptor(storage)) - if (BuildConfig.DEBUG) { - addInterceptor(CurlLoggingInterceptor()) - } - }.build() - return AniListRepository(context, okHttp, storage, database) - } + @ScrobblerType(ScrobblerService.ANILIST) storage: ScrobblerStorage, + ): OkHttpClient = baseHttpClient.newBuilder().apply { + authenticator(authenticator) + addInterceptor(AniListInterceptor(storage)) + }.build() @Provides @Singleton diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListAuthenticator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListAuthenticator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListAuthenticator.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt index f2bd44e7f..a1a3da59c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/AniListRepository.kt @@ -23,7 +23,10 @@ import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser +import javax.inject.Inject +import javax.inject.Singleton import kotlin.math.roundToInt private const val REDIRECT_URI = "kotatsu://anilist-auth" @@ -34,10 +37,11 @@ private const val REQUEST_QUERY = "query" private const val REQUEST_MUTATION = "mutation" private const val KEY_SCORE_FORMAT = "score_format" -class AniListRepository( +@Singleton +class AniListRepository @Inject constructor( @ApplicationContext context: Context, - private val okHttp: OkHttpClient, - private val storage: ScrobblerStorage, + @ScrobblerType(ScrobblerService.ANILIST) private val okHttp: OkHttpClient, + @ScrobblerType(ScrobblerService.ANILIST) private val storage: ScrobblerStorage, private val db: MangaDatabase, ) : ScrobblerRepository { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt index 2683d01b4..47b7bcadf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/data/ScoreFormat.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.scrobbling.anilist.data -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug enum class ScoreFormat { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/domain/AniListScrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/domain/AniListScrobbler.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/anilist/domain/AniListScrobbler.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/anilist/domain/AniListScrobbler.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerRepository.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerRepository.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerStorage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerStorage.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerStorage.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblerStorage.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/data/ScrobblingEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt index 5eb3ebec7..52cb7a107 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/Scrobbler.kt @@ -10,7 +10,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.util.ext.findKeyByValue +import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo @@ -18,9 +21,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus -import org.koitharu.kotatsu.utils.ext.findKeyByValue -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.util.EnumMap abstract class Scrobbler( @@ -53,7 +54,7 @@ abstract class Scrobbler( return repository.loadUser() } - suspend fun logout() { + fun logout() { repository.logout() } @@ -123,7 +124,7 @@ abstract class Scrobbler( rating = rating, title = mangaInfo.name, coverUrl = mangaInfo.cover, - description = mangaInfo.descriptionHtml.parseAsHtml(), + description = mangaInfo.descriptionHtml.parseAsHtml().sanitize(), externalUrl = mangaInfo.url, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerManga.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerMangaInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerMangaInfo.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerMangaInfo.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerMangaInfo.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerType.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerType.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerType.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblerUser.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingInfo.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingStatus.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingStatus.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingStatus.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/domain/model/ScrobblingStatus.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt index ccfcc2186..0afdf0dcb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigActivity.kt @@ -6,16 +6,20 @@ import android.os.Bundle import android.view.View import androidx.activity.viewModels import androidx.core.graphics.Insets -import androidx.core.view.isVisible import androidx.core.view.updatePadding import coil.ImageLoader import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivityScrobblerConfigBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService @@ -23,10 +27,8 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.ui.config.adapter.ScrobblingMangaAdapter import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest import javax.inject.Inject +import com.google.android.material.R as materialR @AndroidEntryPoint class ScrobblerConfigActivity : BaseActivity(), @@ -47,7 +49,7 @@ class ScrobblerConfigActivity : BaseActivity(), supportActionBar?.setDisplayHomeAsUpEnabled(true) val listAdapter = ScrobblingMangaAdapter(this, coil, this) - with(binding.recyclerView) { + with(viewBinding.recyclerView) { adapter = listAdapter setHasFixedSize(true) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) @@ -59,13 +61,13 @@ class ScrobblerConfigActivity : BaseActivity(), ) addItemDecoration(decoration) } - binding.imageViewAvatar.setOnClickListener(this) + viewBinding.imageViewAvatar.setOnClickListener(this) viewModel.content.observe(this, listAdapter::setItems) viewModel.user.observe(this, this::onUserChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged) - viewModel.onError.observe(this, SnackbarErrorObserver(binding.recyclerView, null)) - viewModel.onLoggedOut.observe(this) { + viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) + viewModel.onLoggedOut.observeEvent(this) { finishAfterTransition() } @@ -81,7 +83,7 @@ class ScrobblerConfigActivity : BaseActivity(), } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( left = insets.left + paddingHorizontal, right = insets.right + paddingHorizontal, bottom = insets.bottom + paddingVertical, @@ -112,17 +114,17 @@ class ScrobblerConfigActivity : BaseActivity(), private fun onUserChanged(user: ScrobblerUser?) { if (user == null) { - binding.imageViewAvatar.disposeImageRequest() - binding.imageViewAvatar.isVisible = false + viewBinding.imageViewAvatar.disposeImageRequest() + viewBinding.imageViewAvatar.setImageResource(materialR.drawable.abc_ic_menu_overflow_material) return } - binding.imageViewAvatar.isVisible = true - binding.imageViewAvatar.newImageRequest(this, user.avatar) + viewBinding.imageViewAvatar.newImageRequest(this, user.avatar) + ?.placeholder(R.drawable.bg_badge_empty) ?.enqueueWith(coil) } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.run { + viewBinding.progressBar.run { if (isLoading) { show() } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt index 84826d62e..029d7ce1e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/ScrobblerConfigViewModel.kt @@ -1,20 +1,26 @@ package org.koitharu.kotatsu.scrobbling.common.ui.config import android.net.Uri -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.onFirst +import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler @@ -22,11 +28,6 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.emitValue -import org.koitharu.kotatsu.utils.ext.onFirst -import org.koitharu.kotatsu.utils.ext.require import javax.inject.Inject @HiltViewModel @@ -40,34 +41,34 @@ class ScrobblerConfigViewModel @Inject constructor( val titleResId = scrobbler.scrobblerService.titleResId - val user = MutableLiveData(null) - val onLoggedOut = SingleLiveEvent() + val user = MutableStateFlow(null) + val onLoggedOut = MutableEventFlow() val content = scrobbler.observeAllScrobblingInfo() .onStart { loadingCounter.increment() } .onFirst { loadingCounter.decrement() } - .catch { errorEvent.postCall(it) } + .catch { errorEvent.call(it) } .map { buildContentList(it) } - .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) init { scrobbler.user - .onEach { user.emitValue(it) } + .onEach { user.value = it } .launchIn(viewModelScope + Dispatchers.Default) } fun onAuthCodeReceived(authCode: String) { launchLoadingJob(Dispatchers.Default) { val newUser = scrobbler.authorize(authCode) - user.emitValue(newUser) + user.value = newUser } } fun logout() { launchLoadingJob(Dispatchers.Default) { scrobbler.logout() - user.emitValue(null) - onLoggedOut.emitCall(Unit) + user.value = null + onLoggedOut.call(Unit) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingHeaderAD.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingHeaderAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingHeaderAD.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt index 04c978f66..483f90b5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAD.kt @@ -4,14 +4,14 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest fun scrobblingMangaAD( clickListener: OnListItemClickListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt index b33887675..4e0c4e913 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/config/adapter/ScrobblingMangaAdapter.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt index 76b5d8de0..ba4a89330 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorSheet.kt @@ -7,19 +7,23 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.widget.SearchView -import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import coil.ImageLoader import com.google.android.material.tabs.TabLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.base.ui.BaseBottomSheet -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener -import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback +import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.parsers.model.Manga @@ -27,14 +31,11 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerMangaSelectionDecoration import org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter.ScrobblerSelectorAdapter -import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.withArgs import javax.inject.Inject @AndroidEntryPoint -class ScrobblingSelectorBottomSheet : - BaseBottomSheet(), +class ScrobblingSelectorSheet : + BaseAdaptiveSheet(), OnListItemClickListener, PaginationScrollListener.Callback, View.OnClickListener, @@ -50,18 +51,19 @@ class ScrobblingSelectorBottomSheet : private val viewModel by viewModels() - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingSelectorBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingSelectorBinding { return SheetScrobblingSelectorBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: SheetScrobblingSelectorBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + disableFitToContents() val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this, this) - val decoration = ScrobblerMangaSelectionDecoration(view.context) + val decoration = ScrobblerMangaSelectionDecoration(binding.root.context) with(binding.recyclerView) { adapter = listAdapter addItemDecoration(decoration) - addOnScrollListener(PaginationScrollListener(4, this@ScrobblingSelectorBottomSheet)) + addOnScrollListener(PaginationScrollListener(4, this@ScrobblingSelectorSheet)) } binding.buttonDone.setOnClickListener(this) initOptionsMenu() @@ -72,8 +74,8 @@ class ScrobblingSelectorBottomSheet : decoration.checkedItemId = it binding.recyclerView.invalidateItemDecorations() } - viewModel.onError.observe(viewLifecycleOwner, ::onError) - viewModel.onClose.observe(viewLifecycleOwner) { + viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) + viewModel.onClose.observeEvent(viewLifecycleOwner) { dismiss() } viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { index -> @@ -82,9 +84,7 @@ class ScrobblingSelectorBottomSheet : tab.select() } } - viewModel.searchQuery.observe(viewLifecycleOwner) { - binding.headerBar.subtitle = it - } + viewModel.searchQuery.observe(viewLifecycleOwner, ::onSearchQueryChanged) } override fun onDestroyView() { @@ -133,7 +133,7 @@ class ScrobblingSelectorBottomSheet : return false } viewModel.search(query) - binding.headerBar.menu.findItem(R.id.action_search)?.collapseActionView() + requireViewBinding().toolbar.menu.findItem(R.id.action_search)?.collapseActionView() return true } @@ -149,14 +149,18 @@ class ScrobblingSelectorBottomSheet : if (!isExpanded) { setExpanded(isExpanded = true, isLocked = behavior?.isDraggable == false) } - binding.recyclerView.firstVisibleItemPosition = 0 + requireViewBinding().recyclerView.firstVisibleItemPosition = 0 } private fun openSearch() { - val menuItem = binding.headerBar.menu.findItem(R.id.action_search) ?: return + val menuItem = requireViewBinding().toolbar.menu.findItem(R.id.action_search) ?: return menuItem.expandActionView() } + private fun onSearchQueryChanged(query: String?) { + + } + private fun onError(e: Throwable) { Toast.makeText(requireContext(), e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() if (viewModel.isEmpty) { @@ -165,8 +169,8 @@ class ScrobblingSelectorBottomSheet : } private fun initOptionsMenu() { - binding.headerBar.inflateMenu(R.menu.opt_shiki_selector) - val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search) + requireViewBinding().toolbar.inflateMenu(R.menu.opt_shiki_selector) + val searchMenuItem = requireViewBinding().toolbar.menu.findItem(R.id.action_search) searchMenuItem.setOnActionExpandListener(this) val searchView = searchMenuItem.actionView as SearchView searchView.setOnQueryTextListener(this) @@ -179,11 +183,7 @@ class ScrobblingSelectorBottomSheet : private fun initTabs() { val entries = viewModel.availableScrobblers - val tabs = binding.tabs - if (entries.size <= 1) { - tabs.isVisible = false - return - } + val tabs = requireViewBinding().tabs val selectedId = arguments?.getInt(ARG_SCROBBLER, -1) ?: -1 tabs.removeAllTabs() tabs.clearOnTabSelectedListeners() @@ -198,7 +198,6 @@ class ScrobblingSelectorBottomSheet : tab.select() } } - tabs.isVisible = true } companion object { @@ -207,7 +206,7 @@ class ScrobblingSelectorBottomSheet : private const val ARG_SCROBBLER = "scrobbler" fun show(fm: FragmentManager, manga: Manga, scrobblerService: ScrobblerService?) = - ScrobblingSelectorBottomSheet().withArgs(2) { + ScrobblingSelectorSheet().withArgs(2) { putParcelable(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = false)) if (scrobblerService != null) { putInt(ARG_SCROBBLER, scrobblerService.id) diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt index de9c2898c..b965cdf9c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt @@ -1,7 +1,5 @@ package org.koitharu.kotatsu.scrobbling.common.ui.selector -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.RecyclerView.NO_ID @@ -9,11 +7,19 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState @@ -21,12 +27,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.emitValue -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.require -import org.koitharu.kotatsu.utils.ext.requireValue +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject @HiltViewModel @@ -39,7 +40,7 @@ class ScrobblingSelectorViewModel @Inject constructor( val availableScrobblers = scrobblers.filter { it.isAvailable } - val selectedScrobblerIndex = MutableLiveData(0) + val selectedScrobblerIndex = MutableStateFlow(0) private val scrobblerMangaList = MutableStateFlow>(emptyList()) private val hasNextPage = MutableStateFlow(true) @@ -51,14 +52,14 @@ class ScrobblingSelectorViewModel @Inject constructor( private val currentScrobbler: Scrobbler get() = availableScrobblers[selectedScrobblerIndex.requireValue()] - val content: LiveData> = combine( + val content: StateFlow> = combine( scrobblerMangaList, listError, hasNextPage, ) { list, error, isHasNextPage -> if (list.isNotEmpty()) { if (isHasNextPage) { - list + LoadingFooter + list + LoadingFooter() } else { list } @@ -66,16 +67,16 @@ class ScrobblingSelectorViewModel @Inject constructor( listOf( when { error != null -> errorHint(error) - isHasNextPage -> LoadingFooter + isHasNextPage -> LoadingFooter() else -> emptyResultsHint() }, ) } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) - val selectedItemId = MutableLiveData(NO_ID) - val searchQuery = MutableLiveData(manga.title) - val onClose = SingleLiveEvent() + val selectedItemId = MutableStateFlow(NO_ID) + val searchQuery = MutableStateFlow(manga.title) + val onClose = MutableEventFlow() val isEmpty: Boolean get() = scrobblerMangaList.value.isEmpty() @@ -130,13 +131,13 @@ class ScrobblingSelectorViewModel @Inject constructor( if (doneJob?.isActive == true) { return } - val targetId = selectedItemId.value ?: NO_ID + val targetId = selectedItemId.value if (targetId == NO_ID) { onClose.call(Unit) } doneJob = launchJob(Dispatchers.Default) { currentScrobbler.linkManga(manga.id, targetId) - onClose.emitCall(Unit) + onClose.call(Unit) } } @@ -155,7 +156,7 @@ class ScrobblingSelectorViewModel @Inject constructor( try { val info = currentScrobbler.getScrobblingInfoOrNull(manga.id) if (info != null) { - selectedItemId.emitValue(info.targetId) + selectedItemId.value = info.targetId } } finally { loadList(append = false) diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt index 90926f9fa..ea14d0a6c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerHintAD.kt @@ -1,13 +1,13 @@ package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.setTextAndVisible -import org.koitharu.kotatsu.utils.ext.textAndVisible fun scrobblerHintAD( listener: ListStateHolderListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt index 5b74f913a..4fa7f1a6c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt @@ -7,9 +7,9 @@ import android.graphics.RectF import android.view.View import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID +import org.koitharu.kotatsu.core.util.ext.getItem import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga -import org.koitharu.kotatsu.utils.ext.getItem class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt index e3d7af6c6..c39a79846 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblerSelectorAdapter.kt @@ -4,11 +4,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint import kotlin.jvm.internal.Intrinsics @@ -34,6 +35,7 @@ class ScrobblerSelectorAdapter( oldItem === newItem -> true oldItem is ScrobblerManga && newItem is ScrobblerManga -> oldItem.id == newItem.id oldItem is ScrobblerHint && newItem is ScrobblerHint -> oldItem.textPrimary == newItem.textPrimary + oldItem is LoadingFooter && newItem is LoadingFooter -> oldItem.key == newItem.key else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt index 79487a484..28b73ce01 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/adapter/ScrobblingMangaAD.kt @@ -4,14 +4,14 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.textAndVisible fun scrobblingMangaAD( lifecycleOwner: LifecycleOwner, diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/model/ScrobblerHint.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/model/ScrobblerHint.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/common/ui/selector/model/ScrobblerHint.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/model/ScrobblerHint.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt index f17d51ea7..877cbabf5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt @@ -20,18 +20,22 @@ import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import java.security.SecureRandom +import javax.inject.Inject +import javax.inject.Singleton private const val REDIRECT_URI = "kotatsu://mal-auth" private const val BASE_WEB_URL = "https://myanimelist.net" private const val BASE_API_URL = "https://api.myanimelist.net/v2" private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif" -class MALRepository( +@Singleton +class MALRepository @Inject constructor( @ApplicationContext context: Context, - private val okHttp: OkHttpClient, - private val storage: ScrobblerStorage, + @ScrobblerType(ScrobblerService.MAL) private val okHttp: OkHttpClient, + @ScrobblerType(ScrobblerService.MAL) private val storage: ScrobblerStorage, private val db: MangaDatabase, ) : ScrobblerRepository { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriAuthenticator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriAuthenticator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriAuthenticator.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt index 9e4c90289..e61791deb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/data/ShikimoriRepository.kt @@ -9,6 +9,7 @@ import okhttp3.Request import org.json.JSONObject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.util.ext.toRequestBody import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.json.getStringOrNull @@ -22,17 +23,20 @@ import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerMangaInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser -import org.koitharu.kotatsu.utils.ext.toRequestBody +import javax.inject.Inject +import javax.inject.Singleton private const val REDIRECT_URI = "kotatsu://shikimori-auth" private const val BASE_URL = "https://shikimori.me/" private const val MANGA_PAGE_SIZE = 10 -class ShikimoriRepository( +@Singleton +class ShikimoriRepository @Inject constructor( @ApplicationContext context: Context, - private val okHttp: OkHttpClient, - private val storage: ScrobblerStorage, + @ScrobblerType(ScrobblerService.SHIKIMORI) private val okHttp: OkHttpClient, + @ScrobblerType(ScrobblerService.SHIKIMORI) private val storage: ScrobblerStorage, private val db: MangaDatabase, ) : ScrobblerRepository { diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/domain/ShikimoriScrobbler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/domain/ShikimoriScrobbler.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/shikimori/domain/ShikimoriScrobbler.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/shikimori/domain/ShikimoriScrobbler.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 80d3dce0f..79c106a80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -20,8 +20,8 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.levenshteinDistance +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import javax.inject.Inject @Reusable diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt new file mode 100644 index 000000000..bec248475 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -0,0 +1,151 @@ +package org.koitharu.kotatsu.search.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.core.graphics.Insets +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.fragment.app.commit +import com.google.android.material.appbar.AppBarLayout +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.model.titleRes +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible +import org.koitharu.kotatsu.databinding.ActivityMangaListBinding +import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment +import org.koitharu.kotatsu.filter.ui.FilterOwner +import org.koitharu.kotatsu.filter.ui.FilterSheetFragment +import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.local.ui.LocalListFragment +import org.koitharu.kotatsu.main.ui.owners.AppBarOwner +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment + +@AndroidEntryPoint +class MangaListActivity : + BaseActivity(), + AppBarOwner, View.OnClickListener { + + override val appBar: AppBarLayout + get() = viewBinding.appbar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityMangaListBinding.inflate(layoutInflater)) + val tags = intent.getParcelableExtraCompat(EXTRA_TAGS)?.tags + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source + if (source == null) { + finishAfterTransition() + return + } + viewBinding.chipSort?.setOnClickListener(this) + title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title + initList(source, tags) + } + + override fun onWindowInsetsChanged(insets: Insets) { + viewBinding.root.updatePadding( + left = insets.left, + right = insets.right, + ) + viewBinding.cardFilter?.updateLayoutParams { + bottomMargin = marginStart + insets.bottom + } + } + + override fun onClick(v: View) { + when (v.id) { + R.id.chip_sort -> FilterSheetFragment.show(supportFragmentManager) + } + } + + private fun initList(source: MangaSource, tags: Set?) { + val fm = supportFragmentManager + if (fm.findFragmentById(R.id.container) == null) { + fm.commit { + setReorderingAllowed(true) + val fragment = if (source == MangaSource.LOCAL) { + LocalListFragment.newInstance() + } else { + RemoteListFragment.newInstance(source) + } + replace(R.id.container, fragment) + if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { + runOnCommit(ApplyFilterRunnable(fragment, tags)) + } + runOnCommit { initFilter() } + } + } else { + initFilter() + } + } + + private fun initFilter() { + if (viewBinding.containerFilter != null) { + if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.container_filter, FilterSheetFragment::class.java, null) + } + } + } else if (viewBinding.containerFilterHeader != null) { + if (supportFragmentManager.findFragmentById(R.id.container_filter_header) == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.container_filter_header, FilterHeaderFragment::class.java, null) + } + } + } + val filterOwner = FilterOwner.from(this) + val chipSort = viewBinding.chipSort + if (chipSort != null) { + filterOwner.header.observe(this) { + chipSort.setTextAndVisible(it.sortOrder?.titleRes ?: 0) + } + } else { + filterOwner.header.map { + it.textSummary + }.flowOn(Dispatchers.Default) + .observe(this) { + supportActionBar?.subtitle = it + } + } + } + + private class ApplyFilterRunnable( + private val fragment: MangaListFragment, + private val tags: Set, + ) : Runnable { + + override fun run() { + checkNotNull(FilterOwner.find(fragment)) { + "Cannot find FilterOwner" + }.applyFilter(tags) + } + } + + companion object { + + private const val EXTRA_TAGS = "tags" + private const val EXTRA_SOURCE = "source" + + fun newIntent(context: Context, tags: Set) = Intent(context, MangaListActivity::class.java) + .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) + + fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java) + .putExtra(EXTRA_SOURCE, source) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaSuggestionsProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt index f86f82b3e..cb89e0860 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchActivity.kt @@ -11,12 +11,13 @@ import androidx.core.view.updatePadding import androidx.fragment.app.commit import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.showKeyboard import org.koitharu.kotatsu.databinding.ActivitySearchBinding import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel -import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat -import org.koitharu.kotatsu.utils.ext.showKeyboard @AndroidEntryPoint class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener { @@ -34,7 +35,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery val query = intent.getStringExtra(EXTRA_QUERY) supportActionBar?.setDisplayHomeAsUpEnabled(true) searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged) - with(binding.searchView) { + with(viewBinding.searchView) { queryHint = getString(R.string.search_on_s, source.title) setOnQueryTextListener(this@SearchActivity) @@ -48,11 +49,11 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery } override fun onWindowInsetsChanged(insets: Insets) { - binding.toolbar.updatePadding( + viewBinding.toolbar.updatePadding( left = insets.left, right = insets.right, ) - binding.container.updatePadding( + viewBinding.container.updatePadding( bottom = insets.bottom, ) } @@ -67,7 +68,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery setReorderingAllowed(true) replace(R.id.container, SearchFragment.newInstance(source, q)) } - binding.searchView.clearFocus() + viewBinding.searchView.clearFocus() searchSuggestionViewModel.saveQuery(q) return true } @@ -75,13 +76,13 @@ class SearchActivity : BaseActivity(), SearchView.OnQuery override fun onQueryTextChange(newText: String?): Boolean = false private fun onIncognitoModeChanged(isIncognito: Boolean) { - var options = binding.searchView.imeOptions + var options = viewBinding.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 + viewBinding.searchView.imeOptions = options } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt index b5fa580fe..6e8b9683a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchFragment.kt @@ -5,10 +5,10 @@ import androidx.appcompat.view.ActionMode import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint class SearchFragment : MangaListFragment() { diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 8ce2d3d90..a1ad1f52a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -7,12 +7,16 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -22,8 +26,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.require import javax.inject.Inject @HiltViewModel @@ -31,7 +33,7 @@ class SearchViewModel @Inject constructor( savedStateHandle: SavedStateHandle, repositoryFactory: MangaRepository.Factory, settings: AppSettings, - private val tagHighlighter: MangaTagHighlighter, + private val extraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { @@ -44,7 +46,7 @@ class SearchViewModel @Inject constructor( override val content = combine( mangaList, - listModeFlow, + listMode, listError, hasNextPage, ) { list, mode, error, hasNext -> @@ -62,15 +64,15 @@ class SearchViewModel @Inject constructor( else -> { val result = ArrayList(list.size + 1) - list.toUi(result, mode, tagHighlighter) + list.toUi(result, mode, extraProvider) when { error != null -> result += error.toErrorFooter() - hasNext -> result += LoadingFooter + hasNext -> result += LoadingFooter() } result } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { loadList(append = false) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index 96ab9c1b2..3871a0cfc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -14,28 +14,30 @@ import androidx.core.view.updatePadding import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.list.ListSelectionController -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import javax.inject.Inject @AndroidEntryPoint @@ -59,10 +61,8 @@ class MultiSearchActivity : setContentView(ActivitySearchMultiBinding.inflate(layoutInflater)) window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar) - val itemCLickListener = object : OnListItemClickListener { - override fun onItemClick(item: MultiSearchListModel, view: View) { - startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value)) - } + val itemCLickListener = OnListItemClickListener { item, view -> + startActivity(SearchActivity.newIntent(view.context, item.source, viewModel.query.value)) } val sizeResolver = ItemSizeResolver(resources, settings) val selectionDecoration = MangaSelectionDecoration(this) @@ -80,8 +80,8 @@ class MultiSearchActivity : sizeResolver = sizeResolver, selectionDecoration = selectionDecoration, ) - binding.recyclerView.adapter = adapter - binding.recyclerView.setHasFixedSize(true) + viewBinding.recyclerView.adapter = adapter + viewBinding.recyclerView.setHasFixedSize(true) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) @@ -90,16 +90,16 @@ class MultiSearchActivity : viewModel.query.observe(this) { title = it } viewModel.list.observe(this) { adapter.items = it } - viewModel.onError.observe(this, SnackbarErrorObserver(binding.recyclerView, null)) - viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(binding.recyclerView)) + viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) + viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView)) } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( bottom = insets.bottom, ) } @@ -117,8 +117,8 @@ class MultiSearchActivity : override fun onReadClick(manga: Manga, view: View) { if (!selectionController.onItemClick(manga.id)) { - val intent = ReaderActivity.newIntent(this, manga) - startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) + val intent = IntentBuilder(this).manga(manga).build() + startActivity(intent, scaleUpActivityOptionsOf(view)) } } @@ -130,7 +130,7 @@ class MultiSearchActivity : } override fun onRetryClick(error: Throwable) { - viewModel.doSearch(viewModel.query.value.orEmpty()) + viewModel.doSearch(viewModel.query.value) } override fun onUpdateFilter(tags: Set) = Unit @@ -142,7 +142,7 @@ class MultiSearchActivity : override fun onListHeaderClick(item: ListHeader, view: View) = Unit override fun onSelectionChanged(controller: ListSelectionController, count: Int) { - binding.recyclerView.invalidateNestedItemDecorations() + viewBinding.recyclerView.invalidateNestedItemDecorations() } override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { @@ -159,7 +159,7 @@ class MultiSearchActivity : } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(supportFragmentManager, collectSelectedItems()) + FavouriteCategoriesSheet.show(supportFragmentManager, collectSelectedItems()) mode.finish() true } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt index f8801ca11..8f6ed856e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchListModel.kt @@ -8,6 +8,7 @@ class MultiSearchListModel( val source: MangaSource, val hasMore: Boolean, val list: List, + val error: Throwable?, ) : ListModel { override fun equals(other: Any?): Boolean { @@ -19,14 +20,14 @@ class MultiSearchListModel( if (source != other.source) return false if (hasMore != other.hasMore) return false if (list != other.list) return false - - return true + return error == other.error } override fun hashCode(): Int { var result = source.hashCode() result = 31 * result + hasMore.hashCode() result = 31 * result + list.hashCode() + result = 31 * result + (error?.hashCode() ?: 0) return result } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index 72e6f777d..06d28f85a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -1,7 +1,5 @@ package org.koitharu.kotatsu.search.ui.multi -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,27 +10,30 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus +import kotlinx.coroutines.withTimeout import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.exceptions.CompositeException import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.emitValue -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject private const val MAX_PARALLELISM = 4 @@ -41,6 +42,7 @@ private const val MIN_HAS_MORE_ITEMS = 8 @HiltViewModel class MultiSearchViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val extraProvider: ListExtraProvider, private val settings: AppSettings, private val mangaRepositoryFactory: MangaRepository.Factory, private val downloadScheduler: DownloadWorker.Scheduler, @@ -49,20 +51,17 @@ class MultiSearchViewModel @Inject constructor( private var searchJob: Job? = null private val listData = MutableStateFlow>(emptyList()) private val loadingData = MutableStateFlow(false) - private var listError = MutableStateFlow(null) - val onDownloadStarted = SingleLiveEvent() + val onDownloadStarted = MutableEventFlow() - val query = MutableLiveData(savedStateHandle.get(MultiSearchActivity.EXTRA_QUERY).orEmpty()) - val list: LiveData> = combine( + val query = MutableStateFlow(savedStateHandle.get(MultiSearchActivity.EXTRA_QUERY).orEmpty()) + val list: StateFlow> = combine( listData, loadingData, - listError, - ) { list, loading, error -> + ) { list, loading -> when { list.isEmpty() -> listOf( when { loading -> LoadingState - error != null -> error.toErrorState(canRetry = true) else -> EmptyState( icon = R.drawable.ic_empty_common, textPrimary = R.string.nothing_found, @@ -72,13 +71,13 @@ class MultiSearchViewModel @Inject constructor( }, ) - loading -> list + LoadingFooter + loading -> list + LoadingFooter() else -> list } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { - doSearch(query.value.orEmpty()) + doSearch(query.value) } fun getItems(ids: Set): Set { @@ -98,15 +97,12 @@ class MultiSearchViewModel @Inject constructor( searchJob = launchJob(Dispatchers.Default) { prevJob?.cancelAndJoin() try { - listError.value = null listData.value = emptyList() loadingData.value = true - query.emitValue(q) + query.value = q searchImpl(q) } catch (e: CancellationException) { throw e - } catch (e: Throwable) { - listError.value = e } finally { loadingData.value = false } @@ -116,7 +112,7 @@ class MultiSearchViewModel @Inject constructor( fun download(items: Set) { launchJob(Dispatchers.Default) { downloadScheduler.schedule(items) - onDownloadStarted.emitCall(Unit) + onDownloadStarted.call(Unit) } } @@ -126,35 +122,28 @@ class MultiSearchViewModel @Inject constructor( val deferredList = sources.map { source -> async(dispatcher) { runCatchingCancellable { - val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q) - .toUi(ListMode.GRID, null) - if (list.isNotEmpty()) { - MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list) - } else { - null + withTimeout(8_000) { + mangaRepositoryFactory.create(source).getList(offset = 0, query = q) + .toUi(ListMode.GRID, extraProvider) } - }.onFailure { - it.printStackTraceDebug() - } + }.fold( + onSuccess = { list -> + if (list.isEmpty()) { + null + } else { + MultiSearchListModel(source, list.size > MIN_HAS_MORE_ITEMS, list, null) + } + }, + onFailure = { error -> + error.printStackTraceDebug() + MultiSearchListModel(source, true, emptyList(), error) + }, + ) } } - - val errors = ArrayList() for (deferred in deferredList) { - deferred.await() - .onSuccess { item -> - if (item != null) { - listData.update { x -> x + item } - } - }.onFailure { - errors.add(it) - } - } - if (listData.value.isEmpty()) { - when (errors.size) { - 0 -> Unit - 1 -> throw errors[0] - else -> throw CompositeException(errors) + deferred.await()?.let { item -> + listData.update { x -> x + item } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt index 179957661..5abb41e8c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/MultiSearchAdapter.kt @@ -5,7 +5,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -14,6 +14,7 @@ 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.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.search.ui.multi.MultiSearchListModel import kotlin.jvm.internal.Intrinsics @@ -54,6 +55,10 @@ class MultiSearchAdapter( oldItem.source == newItem.source } + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + else -> oldItem.javaClass == newItem.javaClass } } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt index 71b90aa10..9bd049052 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.search.ui.multi.adapter +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView.RecycledViewPool @@ -7,9 +8,11 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemListGroupBinding import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration @@ -27,12 +30,12 @@ fun searchResultsAD( listener: OnListItemClickListener, itemClickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) } + { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) }, ) { binding.recyclerView.setRecycledViewPool(sharedPool) val adapter = ListDelegationAdapter( - mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver) + mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver), ) binding.recyclerView.addItemDecoration(selectionDecoration) binding.recyclerView.adapter = adapter @@ -46,5 +49,7 @@ fun searchResultsAD( binding.buttonMore.isVisible = item.hasMore adapter.notifyDataSetChanged() adapter.items = item.list + binding.recyclerView.isGone = item.list.isEmpty() + binding.textViewError.textAndVisible = item.error?.getDisplayMessage(context.resources) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt index c7162aad8..804e00f19 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.search.ui.suggestion import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding @@ -10,12 +9,13 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.ItemTouchHelper import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter -import org.koitharu.kotatsu.utils.ext.addMenuProvider +import javax.inject.Inject @AndroidEntryPoint class SearchSuggestionFragment : @@ -27,19 +27,19 @@ class SearchSuggestionFragment : private val viewModel by activityViewModels() - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentSearchSuggestionBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentSearchSuggestionBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val adapter = SearchSuggestionAdapter( coil = coil, lifecycleOwner = viewLifecycleOwner, listener = requireActivity() as SearchSuggestionListener, ) - addMenuProvider(SearchSuggestionMenuProvider(view.context, viewModel)) + addMenuProvider(SearchSuggestionMenuProvider(binding.root.context, viewModel)) binding.root.adapter = adapter binding.root.setHasFixedSize(true) viewModel.suggestion.observe(viewLifecycleOwner) { @@ -51,7 +51,7 @@ class SearchSuggestionFragment : override fun onWindowInsetsChanged(insets: Insets) { val extraPadding = resources.getDimensionPixelOffset(R.dimen.list_spacing) - binding.root.updatePadding( + requireViewBinding().root.updatePadding( top = extraPadding, right = insets.right, left = insets.left, diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt index 7c65c7c42..84120ad16 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionItemCallback.kt @@ -2,9 +2,9 @@ package org.koitharu.kotatsu.search.ui.suggestion import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.core.util.ext.getItem import org.koitharu.kotatsu.search.ui.suggestion.adapter.SEARCH_SUGGESTION_ITEM_TYPE_QUERY import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem -import org.koitharu.kotatsu.utils.ext.getItem class SearchSuggestionItemCallback( private val listener: SuggestionItemListener, @@ -12,7 +12,7 @@ class SearchSuggestionItemCallback( private val movementFlags = makeMovementFlags( 0, - ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, ) override fun getMovementFlags( @@ -39,4 +39,4 @@ class SearchSuggestionItemCallback( fun onRemoveQuery(query: String) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionMenuProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt index 6853f2d18..d44357b08 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.search.ui.suggestion -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -15,16 +14,15 @@ 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.widgets.ChipsView import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.prefs.observeAsLiveData +import org.koitharu.kotatsu.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem -import org.koitharu.kotatsu.utils.ext.emitValue import javax.inject.Inject private const val DEBOUNCE_TIMEOUT = 500L @@ -42,13 +40,13 @@ class SearchSuggestionViewModel @Inject constructor( private val query = MutableStateFlow("") private var suggestionJob: Job? = null - val isIncognitoModeEnabled = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val isIncognitoModeEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_INCOGNITO_MODE, valueProducer = { isIncognitoModeEnabled }, ) - val suggestion = MutableLiveData>() + val suggestion = MutableStateFlow>(emptyList()) init { setupSuggestion() @@ -98,7 +96,7 @@ class SearchSuggestionViewModel @Inject constructor( buildSearchSuggestion(searchQuery, hiddenSources) }.distinctUntilChanged() .onEach { - suggestion.emitValue(it) + suggestion.value = it }.launchIn(viewModelScope + Dispatchers.Default) } @@ -137,6 +135,7 @@ class SearchSuggestionViewModel @Inject constructor( ChipsView.ChipModel( tint = 0, title = tag.title, + icon = 0, data = tag, isCheckable = false, isChecked = false, diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionQueryAD.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt index 5d713a300..6552643a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceAD.kt @@ -4,14 +4,14 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.core.parser.favicon.faviconUri +import org.koitharu.kotatsu.core.ui.image.FaviconFallbackDrawable +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable fun searchSuggestionSourceAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt index 25e3eaf7d..7360de65c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTagsAD.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.search.ui.suggestion.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem @@ -20,4 +20,4 @@ fun searchSuggestionTagsAD( bind { chipGroup.setChips(item.tags) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt index 992ebfd9b..f678ca53c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionsMangaListAD.kt @@ -9,16 +9,16 @@ import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaGridBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem -import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source fun searchSuggestionMangaListAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt index 572d9c75b..8d720e2d1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.search.ui.suggestion.model -import android.net.Uri -import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.areItemsEquals @@ -42,9 +41,7 @@ sealed interface SearchSuggestionItem { other as RecentQuery - if (query != other.query) return false - - return true + return query == other.query } override fun hashCode(): Int { @@ -64,9 +61,7 @@ sealed interface SearchSuggestionItem { other as Source if (source != other.source) return false - if (isEnabled != other.isEnabled) return false - - return true + return isEnabled == other.isEnabled } override fun hashCode(): Int { @@ -86,9 +81,7 @@ sealed interface SearchSuggestionItem { other as Tags - if (tags != other.tags) return false - - return true + return tags == other.tags } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchBehavior.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchBehavior.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchBehavior.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchBehavior.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt index 2c00a1310..417e48d94 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt @@ -16,9 +16,9 @@ import androidx.annotation.AttrRes import androidx.appcompat.widget.AppCompatEditText import androidx.core.content.ContextCompat import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.drawableEnd +import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener -import org.koitharu.kotatsu.utils.ext.drawableEnd -import org.koitharu.kotatsu.utils.ext.drawableStart import com.google.android.material.R as materialR private const val DRAWABLE_END = 2 diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/widget/SearchToolbar.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt index d46df2738..5905e7f17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -13,22 +13,20 @@ import androidx.core.app.LocaleManagerCompat import androidx.core.view.postDelayed import androidx.preference.ListPreference import androidx.preference.Preference -import androidx.preference.TwoStatePreference import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle +import org.koitharu.kotatsu.core.util.ext.getLocalesConfig +import org.koitharu.kotatsu.core.util.ext.map +import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat +import org.koitharu.kotatsu.core.util.ext.toList import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.parsers.util.toTitleCase -import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.utils.ActivityListPreference import org.koitharu.kotatsu.settings.utils.SliderPreference -import org.koitharu.kotatsu.utils.ext.getLocalesConfig -import org.koitharu.kotatsu.utils.ext.map -import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat -import org.koitharu.kotatsu.utils.ext.toList import java.util.Locale import javax.inject.Inject @@ -50,12 +48,10 @@ class AppearanceSettingsFragment : true } } - preferenceScreen?.findPreference(AppSettings.KEY_LIST_MODE)?.run { + findPreference(AppSettings.KEY_LIST_MODE)?.run { entryValues = ListMode.values().names() setDefaultValueCompat(ListMode.GRID.name) } - findPreference(AppSettings.KEY_PROTECT_APP) - ?.isChecked = !settings.appPassword.isNullOrEmpty() findPreference(AppSettings.KEY_APP_LOCALE)?.run { initLocalePicker(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -93,34 +89,12 @@ class AppearanceSettingsFragment : postRestart() } - AppSettings.KEY_APP_PASSWORD -> { - findPreference(AppSettings.KEY_PROTECT_APP) - ?.isChecked = !settings.appPassword.isNullOrEmpty() - } - AppSettings.KEY_APP_LOCALE -> { AppCompatDelegate.setApplicationLocales(settings.appLocales) } } } - override fun onPreferenceTreeClick(preference: Preference): Boolean { - return when (preference.key) { - AppSettings.KEY_PROTECT_APP -> { - val pref = (preference as? TwoStatePreference ?: return false) - if (pref.isChecked) { - pref.isChecked = false - startActivity(Intent(preference.context, ProtectSetupActivity::class.java)) - } else { - settings.appPassword = null - } - true - } - - else -> super.onPreferenceTreeClick(preference) - } - } - private fun postRestart() { view?.postDelayed(400) { activityRecreationHandle.recreateAll() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt similarity index 57% rename from app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt index 34523393b..0bcd68106 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt @@ -3,65 +3,42 @@ package org.koitharu.kotatsu.settings import android.content.SharedPreferences import android.os.Bundle import android.view.View -import androidx.preference.ListPreference import androidx.preference.Preference -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog -import org.koitharu.kotatsu.core.cache.ContentCache -import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.ui.dialog.StorageSelectDialog +import org.koitharu.kotatsu.core.util.ext.getStorageName +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.local.data.LocalStorageManager -import org.koitharu.kotatsu.parsers.util.names -import org.koitharu.kotatsu.utils.ext.getStorageName -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import javax.inject.Inject @AndroidEntryPoint -class ContentSettingsFragment : - BasePreferenceFragment(R.string.content), +class DownloadsSettingsFragment : + BasePreferenceFragment(R.string.downloads), SharedPreferences.OnSharedPreferenceChangeListener, StorageSelectDialog.OnStorageSelectListener { @Inject lateinit var storageManager: LocalStorageManager - @Inject - lateinit var contentCache: ContentCache - @Inject lateinit var downloadsScheduler: DownloadWorker.Scheduler override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_content) - findPreference(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled - findPreference(AppSettings.KEY_DOH)?.run { - entryValues = arrayOf( - DoHProvider.NONE, - DoHProvider.GOOGLE, - DoHProvider.CLOUDFLARE, - DoHProvider.ADGUARD, - ).names() - setDefaultValueCompat(DoHProvider.NONE.name) - } + addPreferencesFromResource(R.xml.pref_downloads) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() - findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( - if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled, - ) - bindRemoteSourcesSummary() settings.subscribe(this) } @@ -76,23 +53,9 @@ class ContentSettingsFragment : findPreference(key)?.bindStorageName() } - AppSettings.KEY_SUGGESTIONS -> { - findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( - if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled, - ) - } - - AppSettings.KEY_SOURCES_HIDDEN -> { - bindRemoteSourcesSummary() - } - AppSettings.KEY_DOWNLOADS_WIFI -> { updateDownloadsConstraints() } - - AppSettings.KEY_SSL_BYPASS -> { - Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show() - } } } @@ -123,13 +86,6 @@ class ContentSettingsFragment : } } - private fun bindRemoteSourcesSummary() { - findPreference(AppSettings.KEY_REMOTE_SOURCES)?.run { - val total = settings.remoteMangaSources.size - summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total) - } - } - private fun updateDownloadsConstraints() { val preference = findPreference(AppSettings.KEY_DOWNLOADS_WIFI) viewLifecycleScope.launch { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt new file mode 100644 index 000000000..48d6f6407 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.settings + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.ListPreference +import androidx.preference.Preference +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.network.DoHProvider +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat +import org.koitharu.kotatsu.parsers.util.names +import java.net.Proxy +import javax.inject.Inject + +@AndroidEntryPoint +class NetworkSettingsFragment : + BasePreferenceFragment(R.string.network), + SharedPreferences.OnSharedPreferenceChangeListener { + + @Inject + lateinit var contentCache: ContentCache + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_network) + findPreference(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled + findPreference(AppSettings.KEY_DOH)?.run { + entryValues = arrayOf( + DoHProvider.NONE, + DoHProvider.GOOGLE, + DoHProvider.CLOUDFLARE, + DoHProvider.ADGUARD, + ).names() + setDefaultValueCompat(DoHProvider.NONE.name) + } + bindProxySummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_SSL_BYPASS -> { + Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show() + } + + AppSettings.KEY_PROXY_TYPE, + AppSettings.KEY_PROXY_ADDRESS, + AppSettings.KEY_PROXY_PORT -> { + bindProxySummary() + } + } + } + + private fun bindProxySummary() { + findPreference(AppSettings.KEY_PROXY)?.run { + val type = settings.proxyType + val address = settings.proxyAddress + val port = settings.proxyPort + summary = if (type == Proxy.Type.DIRECT || address.isNullOrEmpty() || port == 0) { + context.getString(R.string.disabled) + } else { + "$address:$port" + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt index dedac9988..a75535bcc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt @@ -6,8 +6,8 @@ import android.os.Bundle import android.view.View import androidx.preference.Preference import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.settings.utils.RingtonePickContract class NotificationSettingsLegacyFragment : @@ -56,6 +56,7 @@ class NotificationSettingsLegacyFragment : ringtonePickContract.launch(settings.notificationSound) true } + else -> super.onPreferenceTreeClick(preference) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt new file mode 100644 index 000000000..4125d46e2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.settings + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.settings.utils.EditTextBindListener +import org.koitharu.kotatsu.settings.utils.PasswordSummaryProvider +import org.koitharu.kotatsu.settings.utils.validation.DomainValidator +import org.koitharu.kotatsu.settings.utils.validation.PortNumberValidator +import java.net.Proxy + +@AndroidEntryPoint +class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy), + SharedPreferences.OnSharedPreferenceChangeListener { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_proxy) + findPreference(AppSettings.KEY_PROXY_ADDRESS)?.setOnBindEditTextListener( + EditTextBindListener( + inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI, + hint = null, + validator = DomainValidator(), + ), + ) + findPreference(AppSettings.KEY_PROXY_PORT)?.setOnBindEditTextListener( + EditTextBindListener( + inputType = EditorInfo.TYPE_CLASS_NUMBER, + hint = null, + validator = PortNumberValidator(), + ), + ) + findPreference(AppSettings.KEY_PROXY_PASSWORD)?.let { pref -> + pref.setOnBindEditTextListener( + EditTextBindListener( + inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD, + hint = null, + validator = null, + ), + ) + pref.summaryProvider = PasswordSummaryProvider() + } + updateDependencies() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_PROXY_TYPE -> updateDependencies() + } + } + + private fun updateDependencies() { + val isProxyEnabled = settings.proxyType != Proxy.Type.DIRECT + findPreference(AppSettings.KEY_PROXY_ADDRESS)?.isEnabled = isProxyEnabled + findPreference(AppSettings.KEY_PROXY_PORT)?.isEnabled = isProxyEnabled + findPreference(AppSettings.KEY_PROXY_AUTH)?.isEnabled = isProxyEnabled + findPreference(AppSettings.KEY_PROXY_LOGIN)?.isEnabled = isProxyEnabled + findPreference(AppSettings.KEY_PROXY_PASSWORD)?.isEnabled = isProxyEnabled + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt index 0f1ab7c74..672073926 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt @@ -8,13 +8,13 @@ import androidx.preference.MultiSelectListPreference import androidx.preference.Preference import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider -import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat @AndroidEntryPoint class ReaderSettingsFragment : diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt new file mode 100644 index 000000000..63c2a1d75 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt @@ -0,0 +1,56 @@ +package org.koitharu.kotatsu.settings + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.annotation.StringRes +import androidx.preference.Preference +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment + +class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnSharedPreferenceChangeListener { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_root) + bindPreferenceSummary("appearance", R.string.theme, R.string.list_mode, R.string.language) + bindPreferenceSummary("reader", R.string.read_mode, R.string.scale_mode, R.string.switch_pages) + bindPreferenceSummary("network", R.string.proxy, R.string.dns_over_https, R.string.prefetch_content) + bindPreferenceSummary("userdata", R.string.protect_application, R.string.backup_restore, R.string.data_deletion) + bindPreferenceSummary("downloads", R.string.manga_save_location, R.string.downloads_wifi_only) + bindPreferenceSummary("tracker", R.string.track_sources, R.string.notifications_settings) + bindPreferenceSummary("services", R.string.suggestions, R.string.sync, R.string.tracking) + findPreference("about")?.summary = getString(R.string.app_version, BuildConfig.VERSION_NAME) + bindRemoteSourcesSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_SOURCES_HIDDEN -> { + bindRemoteSourcesSummary() + } + } + } + + private fun bindPreferenceSummary(key: String, @StringRes vararg items: Int) { + findPreference(key)?.summary = items.joinToString { getString(it) } + } + + private fun bindRemoteSourcesSummary() { + findPreference(AppSettings.KEY_REMOTE_SOURCES)?.run { + val total = settings.remoteMangaSources.size + summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt index 2cea0ee75..74e04a2a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt @@ -3,8 +3,10 @@ package org.koitharu.kotatsu.settings import android.accounts.AccountManager import android.content.ActivityNotFoundException import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.os.Bundle +import android.view.View import androidx.preference.Preference import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -12,8 +14,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity @@ -21,13 +25,12 @@ import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject @AndroidEntryPoint -class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { +class ServicesSettingsFragment : BasePreferenceFragment(R.string.services), + SharedPreferences.OnSharedPreferenceChangeListener { @Inject lateinit var shikimoriRepository: ShikimoriRepository @@ -43,6 +46,17 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_services) + bindSuggestionsSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() } override fun onResume() { @@ -53,6 +67,13 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { bindSyncSummary() } + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_SUGGESTIONS -> bindSuggestionsSummary() + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { AppSettings.KEY_SHIKIMORI -> { @@ -156,4 +177,10 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { findPreference(AppSettings.KEY_SYNC_SETTINGS)?.isEnabled = account != null } } + + private fun bindSuggestionsSummary() { + findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( + if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled, + ) + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt index 5cecec9ab..7af612d70 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -6,7 +6,9 @@ import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams import androidx.core.graphics.Insets +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -18,16 +20,18 @@ import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.isScrolledToTop +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ActivitySettingsBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.about.AboutSettingsFragment +import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.SourcesListFragment import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment -import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat -import org.koitharu.kotatsu.utils.ext.isScrolledToTop @AndroidEntryPoint class SettingsActivity : @@ -37,21 +41,29 @@ class SettingsActivity : FragmentManager.OnBackStackChangedListener { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivitySettingsBinding.inflate(layoutInflater)) supportActionBar?.setDisplayHomeAsUpEnabled(true) - - if (supportFragmentManager.findFragmentById(R.id.container) == null) { + val isMasterDetails = viewBinding.containerMaster != null + val fm = supportFragmentManager + val currentFragment = fm.findFragmentById(R.id.container) + if (currentFragment == null || (isMasterDetails && currentFragment is RootSettingsFragment)) { openDefaultFragment() } + if (isMasterDetails && fm.findFragmentById(R.id.container_master) == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.container_master, RootSettingsFragment()) + } + } } override fun onTitleChanged(title: CharSequence?, color: Int) { super.onTitleChanged(title, color) - binding.collapsingToolbarLayout?.title = title + viewBinding.collapsingToolbarLayout?.title = title } override fun onStart() { @@ -86,11 +98,10 @@ class SettingsActivity : val fragment = supportFragmentManager.findFragmentById(R.id.container) as? RecyclerViewOwner ?: return val recyclerView = fragment.recyclerView recyclerView.post { - binding.appbar.setExpanded(recyclerView.isScrolledToTop, false) + viewBinding.appbar.setExpanded(recyclerView.isScrolledToTop, false) } } - @Suppress("DEPRECATION") override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference, @@ -98,37 +109,47 @@ class SettingsActivity : val fm = supportFragmentManager val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false) fragment.arguments = pref.extras - fragment.setTargetFragment(caller, 0) - openFragment(fragment) + openFragment(fragment, isFromRoot = caller is RootSettingsFragment) return true } override fun onWindowInsetsChanged(insets: Insets) { - binding.appbar.updatePadding( - left = insets.left, - right = insets.right, - ) - binding.container.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) + viewBinding.cardDetails?.updateLayoutParams { + bottomMargin = marginStart + insets.bottom + } + } + + fun setSectionTitle(title: CharSequence?) { + viewBinding.textViewHeader?.apply { + textAndVisible = title + } ?: setTitle(title ?: getString(R.string.settings)) } - fun openFragment(fragment: Fragment) { + fun openFragment(fragment: Fragment, isFromRoot: Boolean) { + val hasFragment = supportFragmentManager.findFragmentById(R.id.container) != null + val isMasterDetail = viewBinding.containerMaster != null supportFragmentManager.commit { setReorderingAllowed(true) replace(R.id.container, fragment) - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - addToBackStack(null) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN) + if (!isMasterDetail || (hasFragment && !isFromRoot)) { + addToBackStack(null) + } } } private fun openDefaultFragment() { + val hasMaster = viewBinding.containerMaster != null val fragment = when (intent?.action) { ACTION_READER -> ReaderSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() - ACTION_HISTORY -> HistorySettingsFragment() + ACTION_HISTORY -> UserDataSettingsFragment() ACTION_TRACKER -> TrackerSettingsFragment() + ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment() ACTION_SOURCE -> SourceSettingsFragment.newInstance( intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL, ) @@ -138,12 +159,12 @@ class SettingsActivity : when (intent.data?.host) { HOST_ABOUT -> AboutSettingsFragment() HOST_SYNC_SETTINGS -> SyncSettingsFragment() - else -> SettingsHeadersFragment() + else -> null } } - else -> SettingsHeadersFragment() - } + else -> null + } ?: if (hasMaster) AppearanceSettingsFragment() else RootSettingsFragment() supportFragmentManager.commit { setReorderingAllowed(true) replace(R.id.container, fragment) @@ -158,6 +179,7 @@ class SettingsActivity : private const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY" private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST" + private const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS" private const val EXTRA_SOURCE = "source" private const val HOST_ABOUT = "about" private const val HOST_SYNC_SETTINGS = "sync-settings" @@ -184,6 +206,10 @@ class SettingsActivity : Intent(context, SettingsActivity::class.java) .setAction(ACTION_MANAGE_SOURCES) + fun newDownloadsSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_MANAGE_DOWNLOADS) + fun newSourceSettingsIntent(context: Context, source: MangaSource) = Intent(context, SettingsActivity::class.java) .setAction(ACTION_SOURCE) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt index 0ba19a211..f7be5bdcd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt @@ -4,15 +4,15 @@ import android.content.SharedPreferences import android.os.Bundle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import kotlinx.coroutines.launch import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.settings.utils.MultiAutoCompleteTextViewPreference import org.koitharu.kotatsu.settings.utils.TagsAutoCompleteProvider import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker +import javax.inject.Inject @AndroidEntryPoint class SuggestionsSettingsFragment : diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt index 530b8d0ea..60a609307 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt @@ -6,7 +6,7 @@ import androidx.fragment.app.FragmentResultListener import androidx.preference.Preference import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.sync.ui.SyncHostDialogFragment import javax.inject.Inject diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt similarity index 70% rename from app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt index 82f6bc441..2972d126d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt @@ -1,9 +1,16 @@ package org.koitharu.kotatsu.settings +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri import android.os.Bundle import android.view.View +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.Lifecycle import androidx.preference.Preference +import androidx.preference.TwoStatePreference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -13,22 +20,28 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import okhttp3.Cache import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.awaitStateAtLeast +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.search.domain.MangaSearchRepository +import org.koitharu.kotatsu.settings.backup.BackupDialogFragment +import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment +import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.awaitStateAtLeast -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import javax.inject.Inject @AndroidEntryPoint -class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) { +class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privacy), + SharedPreferences.OnSharedPreferenceChangeListener, + ActivityResultCallback { @Inject lateinit var trackerRepo: TrackingRepository @@ -48,10 +61,17 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach @Inject lateinit var shortcutsUpdater: ShortcutsUpdater + private val backupSelectCall = registerForActivityResult( + ActivityResultContracts.OpenDocument(), + this, + ) + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_history) + addPreferencesFromResource(R.xml.pref_user_data) findPreference(AppSettings.KEY_SHORTCUTS)?.isVisible = shortcutsUpdater.isDynamicShortcutsAvailable() + findPreference(AppSettings.KEY_PROTECT_APP) + ?.isChecked = !settings.appPassword.isNullOrEmpty() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -73,6 +93,12 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach pref.summary = pref.context.resources.getQuantityString(R.plurals.items, items, items) } } + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -116,10 +142,53 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach true } + AppSettings.KEY_BACKUP -> { + BackupDialogFragment.show(childFragmentManager) + true + } + + AppSettings.KEY_RESTORE -> { + try { + backupSelectCall.launch(arrayOf("*/*")) + } catch (e: ActivityNotFoundException) { + e.printStackTraceDebug() + Snackbar.make( + listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT, + ).show() + } + true + } + + AppSettings.KEY_PROTECT_APP -> { + val pref = (preference as? TwoStatePreference ?: return false) + if (pref.isChecked) { + pref.isChecked = false + startActivity(Intent(preference.context, ProtectSetupActivity::class.java)) + } else { + settings.appPassword = null + } + true + } + else -> super.onPreferenceTreeClick(preference) } } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_APP_PASSWORD -> { + findPreference(AppSettings.KEY_PROTECT_APP) + ?.isChecked = !settings.appPassword.isNullOrEmpty() + } + } + } + + override fun onActivityResult(result: Uri?) { + if (result != null) { + RestoreDialogFragment.show(childFragmentManager, result) + } + } + private fun clearCache(preference: Preference, cache: CacheDir) { val ctx = preference.context.applicationContext viewLifecycleScope.launch { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index cfede729c..cda8c9fdd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -11,13 +11,15 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.VersionId import org.koitharu.kotatsu.core.github.isStable import org.koitharu.kotatsu.core.logs.FileLogger import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import javax.inject.Inject @AndroidEntryPoint @@ -45,7 +47,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { viewModel.isLoading.observe(viewLifecycleOwner) { findPreference(AppSettings.KEY_APP_UPDATE)?.isEnabled = !it } - viewModel.onUpdateAvailable.observe(viewLifecycleOwner, ::onUpdateAvailable) + viewModel.onUpdateAvailable.observeEvent(viewLifecycleOwner, ::onUpdateAvailable) } override fun onPreferenceTreeClick(preference: Preference): Boolean { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt similarity index 73% rename from app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt index 5289f751c..638ce1e8e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt @@ -1,11 +1,12 @@ package org.koitharu.kotatsu.settings.about import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.github.AppVersion -import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import javax.inject.Inject @HiltViewModel class AboutSettingsViewModel @Inject constructor( @@ -13,7 +14,7 @@ class AboutSettingsViewModel @Inject constructor( ) : BaseViewModel() { val isUpdateSupported = appUpdateRepository.isUpdateSupported() - val onUpdateAvailable = SingleLiveEvent() + val onUpdateAvailable = MutableEventFlow() fun checkForUpdates() { launchLoadingJob { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt index 0043a5ab2..e57058e09 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt @@ -8,7 +8,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.noties.markwon.Markwon import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.github.AppVersion -import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.core.util.FileSize import com.google.android.material.R as materialR class AppUpdateDialog(private val context: Context) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 8acbc7a10..caf25f180 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -3,18 +3,20 @@ package org.koitharu.kotatsu.settings.backup import android.net.Uri import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.DialogProgressBinding -import org.koitharu.kotatsu.utils.ext.getDisplayMessage import java.io.File import java.io.FileOutputStream import kotlin.math.roundToInt @@ -36,19 +38,19 @@ class BackupDialogFragment : AlertDialogFragment() { } } - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = DialogProgressBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.textViewTitle.setText(R.string.create_backup) binding.textViewSubtitle.setText(R.string.processing_) viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged) - viewModel.onBackupDone.observe(viewLifecycleOwner, this::onBackupDone) - viewModel.onError.observe(viewLifecycleOwner, this::onError) + viewModel.onBackupDone.observeEvent(viewLifecycleOwner, this::onBackupDone) + viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) } override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { @@ -67,7 +69,7 @@ class BackupDialogFragment : AlertDialogFragment() { } private fun onProgressChanged(value: Float) { - with(binding.progressBar) { + with(requireViewBinding().progressBar) { isVisible = true val wasIndeterminate = isIndeterminate isIndeterminate = value < 0 @@ -100,6 +102,10 @@ class BackupDialogFragment : AlertDialogFragment() { companion object { - const val TAG = "BackupDialogFragment" + private const val TAG = "BackupDialogFragment" + + fun show(fm: FragmentManager) { + BackupDialogFragment().show(fm, TAG) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupObserver.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupObserver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt index ae1cae8a4..8c60b385f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt @@ -1,13 +1,14 @@ package org.koitharu.kotatsu.settings.backup import android.content.Context -import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipOutput -import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import java.io.File import javax.inject.Inject @@ -17,8 +18,8 @@ class BackupViewModel @Inject constructor( @ApplicationContext context: Context, ) : BaseViewModel() { - val progress = MutableLiveData(-1f) - val onBackupDone = SingleLiveEvent() + val progress = MutableStateFlow(-1f) + val onBackupDone = MutableEventFlow() init { launchLoadingJob { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt similarity index 72% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt index e1e3dee63..c697a63fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt @@ -3,18 +3,20 @@ package org.koitharu.kotatsu.settings.backup import android.net.Uri 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 com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.core.backup.CompositeResult +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.DialogProgressBinding -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.withArgs import kotlin.math.roundToInt @AndroidEntryPoint @@ -22,19 +24,19 @@ class RestoreDialogFragment : AlertDialogFragment() { private val viewModel: RestoreViewModel by viewModels() - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = DialogProgressBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.textViewTitle.setText(R.string.restore_backup) binding.textViewSubtitle.setText(R.string.preparing_) viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged) - viewModel.onRestoreDone.observe(viewLifecycleOwner, this::onRestoreDone) - viewModel.onError.observe(viewLifecycleOwner, this::onError) + viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone) + viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) } override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { @@ -52,7 +54,7 @@ class RestoreDialogFragment : AlertDialogFragment() { } private fun onProgressChanged(value: Float) { - with(binding.progressBar) { + with(requireViewBinding().progressBar) { isVisible = true val wasIndeterminate = isIndeterminate isIndeterminate = value < 0 @@ -86,10 +88,12 @@ class RestoreDialogFragment : AlertDialogFragment() { companion object { const val ARG_FILE = "file" - const val TAG = "RestoreDialogFragment" + private const val TAG = "RestoreDialogFragment" - fun newInstance(uri: Uri) = RestoreDialogFragment().withArgs(1) { - putString(ARG_FILE, uri.toString()) + fun show(fm: FragmentManager, uri: Uri) { + RestoreDialogFragment().withArgs(1) { + putString(ARG_FILE, uri.toString()) + }.show(fm, TAG) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 9efc859cf..01b55c951 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -1,19 +1,20 @@ package org.koitharu.kotatsu.settings.backup import android.content.Context -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runInterruptible -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.backup.BackupEntry import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipInput import org.koitharu.kotatsu.core.backup.CompositeResult -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.toUriOrNull +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.toUriOrNull import java.io.File import java.io.FileNotFoundException import javax.inject.Inject @@ -25,8 +26,8 @@ class RestoreViewModel @Inject constructor( @ApplicationContext context: Context, ) : BaseViewModel() { - val progress = MutableLiveData(-1f) - val onRestoreDone = SingleLiveEvent() + val progress = MutableStateFlow(-1f) + val onRestoreDone = MutableEventFlow() init { launchLoadingJob { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt similarity index 84% rename from app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt index 7c76e4d84..bec4ad77f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.newsources import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels @@ -11,7 +10,8 @@ import coil.ImageLoader import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.DialogOnboardBinding import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem @@ -28,12 +28,12 @@ class NewSourcesDialogFragment : private val viewModel by viewModels() - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogOnboardBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogOnboardBinding { return DialogOnboardBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val adapter = SourcesSelectAdapter(this, coil, viewLifecycleOwner) binding.recyclerView.adapter = adapter binding.textViewTitle.setText(R.string.new_sources_text) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt similarity index 74% rename from app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt index 52151b6a3..1a2a935a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -1,27 +1,23 @@ package org.koitharu.kotatsu.settings.newsources import androidx.core.os.LocaleListCompat -import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.mapToSet import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import org.koitharu.kotatsu.utils.ext.mapToSet +import javax.inject.Inject @HiltViewModel class NewSourcesViewModel @Inject constructor( private val settings: AppSettings, ) : BaseViewModel() { - val sources = MutableLiveData>() + val sources = MutableStateFlow>(buildList()) private val initialList = settings.newSources - init { - buildList() - } - fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { if (isEnabled) { settings.hiddenSources -= item.source.name @@ -34,10 +30,10 @@ class NewSourcesViewModel @Inject constructor( settings.markKnownSources(initialList) } - private fun buildList() { + private fun buildList(): List { val locales = LocaleListCompat.getDefault().mapToSet { it.language } val pendingHidden = HashSet() - sources.value = initialList.map { + return initialList.map { val locale = it.locale val isEnabledByLocale = locale == null || locale in locales if (!isEnabledByLocale) { @@ -49,9 +45,10 @@ class NewSourcesViewModel @Inject constructor( isEnabled = isEnabledByLocale, isDraggable = false, ) - } - if (pendingHidden.isNotEmpty()) { - settings.hiddenSources += pendingHidden + }.also { + if (pendingHidden.isNotEmpty()) { + settings.hiddenSources += pendingHidden + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/SourcesSelectAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index 51b21e2ce..14ac39e54 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -3,20 +3,20 @@ package org.koitharu.kotatsu.settings.onboard import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.showAllowStateLoss +import org.koitharu.kotatsu.core.util.ext.withArgs 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.model.SourceLocale -import org.koitharu.kotatsu.utils.ext.showAllowStateLoss -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint class OnboardDialogFragment : @@ -33,7 +33,7 @@ class OnboardDialogFragment : } } - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = DialogOnboardBinding.inflate(inflater, container, false) @@ -52,8 +52,8 @@ class OnboardDialogFragment : return builder } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val adapter = SourceLocalesAdapter(this) binding.recyclerView.adapter = adapter binding.textViewTitle.setText(R.string.onboard_text) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt index 82a7400b0..4707da3cc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt @@ -1,17 +1,17 @@ package org.koitharu.kotatsu.settings.onboard import androidx.core.os.LocaleListCompat -import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.map +import org.koitharu.kotatsu.core.util.ext.mapToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.onboard.model.SourceLocale -import org.koitharu.kotatsu.utils.ext.map -import org.koitharu.kotatsu.utils.ext.mapToSet import java.util.Locale import javax.inject.Inject @@ -26,7 +26,7 @@ class OnboardViewModel @Inject constructor( private val selectedLocales = locales.keys.toMutableSet() - val list = MutableLiveData?>() + val list = MutableStateFlow?>(null) init { if (settings.isSourcesSelected) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt index f5d5f1aed..3d5dfae35 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleAD.kt @@ -2,10 +2,10 @@ package org.koitharu.kotatsu.settings.onboard.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.setChecked +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding import org.koitharu.kotatsu.settings.onboard.model.SourceLocale -import org.koitharu.kotatsu.utils.ext.setChecked -import org.koitharu.kotatsu.utils.ext.textAndVisible fun sourceLocaleAD( listener: SourceLocaleListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocaleListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/adapter/SourceLocalesAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/model/SourceLocale.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt index 479cbdb5e..cc086c8cd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt @@ -17,7 +17,9 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivitySetupProtectBinding private const val MIN_PASSWORD_LENGTH = 4 @@ -36,29 +38,29 @@ class ProtectSetupActivity : super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) setContentView(ActivitySetupProtectBinding.inflate(layoutInflater)) - binding.editPassword.addTextChangedListener(this) - binding.editPassword.setOnEditorActionListener(this) - binding.buttonNext.setOnClickListener(this) - binding.buttonCancel.setOnClickListener(this) + viewBinding.editPassword.addTextChangedListener(this) + viewBinding.editPassword.setOnEditorActionListener(this) + viewBinding.buttonNext.setOnClickListener(this) + viewBinding.buttonCancel.setOnClickListener(this) - binding.switchBiometric.isChecked = viewModel.isBiometricEnabled - binding.switchBiometric.setOnCheckedChangeListener(this) + viewBinding.switchBiometric.isChecked = viewModel.isBiometricEnabled + viewBinding.switchBiometric.setOnCheckedChangeListener(this) viewModel.isSecondStep.observe(this, this::onStepChanged) - viewModel.onPasswordSet.observe(this) { + viewModel.onPasswordSet.observeEvent(this) { finishAfterTransition() } - viewModel.onPasswordMismatch.observe(this) { - binding.editPassword.error = getString(R.string.passwords_mismatch) + viewModel.onPasswordMismatch.observeEvent(this) { + viewBinding.editPassword.error = getString(R.string.passwords_mismatch) } - viewModel.onClearText.observe(this) { - binding.editPassword.text?.clear() + viewModel.onClearText.observeEvent(this) { + viewBinding.editPassword.text?.clear() } } override fun onWindowInsetsChanged(insets: Insets) { val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding) - binding.root.setPadding( + viewBinding.root.setPadding( basePadding + insets.left, basePadding + insets.top, basePadding + insets.right, @@ -70,7 +72,7 @@ class ProtectSetupActivity : when (v.id) { R.id.button_cancel -> finish() R.id.button_next -> viewModel.onNextClick( - password = binding.editPassword.text?.toString() ?: return, + password = viewBinding.editPassword.text?.toString() ?: return, ) } } @@ -80,8 +82,8 @@ class ProtectSetupActivity : } override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { - return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) { - binding.buttonNext.performClick() + return if (actionId == EditorInfo.IME_ACTION_DONE && viewBinding.buttonNext.isEnabled) { + viewBinding.buttonNext.performClick() true } else { false @@ -93,22 +95,22 @@ class ProtectSetupActivity : override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable?) { - binding.editPassword.error = null + viewBinding.editPassword.error = null val isEnoughLength = (s?.length ?: 0) >= MIN_PASSWORD_LENGTH - binding.buttonNext.isEnabled = isEnoughLength - binding.layoutPassword.isHelperTextEnabled = + viewBinding.buttonNext.isEnabled = isEnoughLength + viewBinding.layoutPassword.isHelperTextEnabled = !isEnoughLength || viewModel.isSecondStep.value == true } private fun onStepChanged(isSecondStep: Boolean) { - binding.buttonCancel.isGone = isSecondStep - binding.switchBiometric.isVisible = isSecondStep && isBiometricAvailable() + viewBinding.buttonCancel.isGone = isSecondStep + viewBinding.switchBiometric.isVisible = isSecondStep && isBiometricAvailable() if (isSecondStep) { - binding.layoutPassword.helperText = getString(R.string.repeat_password) - binding.buttonNext.setText(R.string.confirm) + viewBinding.layoutPassword.helperText = getString(R.string.repeat_password) + viewBinding.buttonNext.setText(R.string.confirm) } else { - binding.layoutPassword.helperText = getString(R.string.password_length_hint) - binding.buttonNext.setText(R.string.next) + viewBinding.layoutPassword.helperText = getString(R.string.password_length_hint) + viewBinding.buttonNext.setText(R.string.next) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt index 351ccc360..73b5597b3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt @@ -2,13 +2,17 @@ package org.koitharu.kotatsu.settings.protect import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.parsers.util.md5 -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel @@ -20,10 +24,10 @@ class ProtectSetupViewModel @Inject constructor( val isSecondStep = firstPassword.map { it != null - }.asFlowLiveData(viewModelScope.coroutineContext, false) - val onPasswordSet = SingleLiveEvent() - val onPasswordMismatch = SingleLiveEvent() - val onClearText = SingleLiveEvent() + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) + val onPasswordSet = MutableEventFlow() + val onPasswordMismatch = MutableEventFlow() + val onClearText = MutableEventFlow() val isBiometricEnabled get() = settings.isBiometricProtectionEnabled diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt index 6a83af617..f0c239c23 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.settings +package org.koitharu.kotatsu.settings.sources import android.view.inputmethod.EditorInfo import androidx.preference.EditTextPreference @@ -11,6 +11,8 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference import org.koitharu.kotatsu.settings.utils.EditTextBindListener import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider +import org.koitharu.kotatsu.settings.utils.validation.DomainValidator +import org.koitharu.kotatsu.settings.utils.validation.HeaderValidator fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMangaRepository) { val configKeys = repository.getConfigKeys() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt new file mode 100644 index 000000000..2fbae0771 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt @@ -0,0 +1,100 @@ +package org.koitharu.kotatsu.settings.sources + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.preference.Preference +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity + +@AndroidEntryPoint +class SourceSettingsFragment : BasePreferenceFragment(0) { + + private val viewModel: SourceSettingsViewModel by viewModels() + private val exceptionResolver = ExceptionResolver(this) + + override fun onResume() { + super.onResume() + setTitle(viewModel.source.title) + viewModel.onResume() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.sharedPreferencesName = viewModel.source.name + addPreferencesFromResource(R.xml.pref_source) + addPreferencesFromRepository(viewModel.repository) + + findPreference(KEY_AUTH)?.run { + val authProvider = viewModel.repository.getAuthProvider() + isVisible = authProvider != null + isEnabled = authProvider?.isAuthorized == false + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.username.observe(viewLifecycleOwner) { username -> + findPreference(KEY_AUTH)?.summary = username?.let { + getString(R.string.logged_in_as, it) + } + } + viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) + viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> + findPreference(KEY_AUTH)?.isEnabled = !isLoading + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + KEY_AUTH -> { + startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source)) + true + } + + else -> super.onPreferenceTreeClick(preference) + } + } + + private fun onError(error: Throwable) { + val snackbar = Snackbar.make( + listView ?: return, + error.getDisplayMessage(resources), + Snackbar.LENGTH_INDEFINITE, + ) + if (ExceptionResolver.canResolve(error)) { + snackbar.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } + } + snackbar.show() + } + + private fun resolveError(error: Throwable) { + view ?: return + viewLifecycleScope.launch { + if (exceptionResolver.resolve(error)) { + viewModel.onResume() + } + } + } + + companion object { + + private const val KEY_AUTH = "auth" + + const val EXTRA_SOURCE = "source" + + fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) { + putSerializable(EXTRA_SOURCE, source) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt new file mode 100644 index 000000000..173c53b69 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsViewModel.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.settings.sources + +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.model.MangaSource +import javax.inject.Inject + +@HiltViewModel +class SourceSettingsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + mangaRepositoryFactory: MangaRepository.Factory, +) : BaseViewModel() { + + val source = savedStateHandle.require(SourceSettingsFragment.EXTRA_SOURCE) + val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository + + val username = MutableStateFlow(null) + private var usernameLoadJob: Job? = null + + init { + loadUsername() + } + + fun onResume() { + if (usernameLoadJob?.isActive != true) { + loadUsername() + } + } + + private fun loadUsername() { + launchLoadingJob(Dispatchers.Default) { + try { + username.value = null + username.value = repository.getAuthProvider()?.getUsername() + } catch (_: AuthRequiredException) { + } + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt index 2fdd8b92f..413bea9a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets @@ -17,19 +16,19 @@ import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner -import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.getItem +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.settings.SettingsHeadersFragment -import org.koitharu.kotatsu.settings.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.getItem import javax.inject.Inject @AndroidEntryPoint @@ -45,20 +44,15 @@ class SourcesListFragment : private val viewModel by viewModels() override val recyclerView: RecyclerView - get() = binding.recyclerView + get() = requireViewBinding().recyclerView - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentSettingsSourcesBinding.inflate(inflater, container, false) - override fun onResume() { - super.onResume() - activity?.setTitle(R.string.remote_sources) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentSettingsSourcesBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val sourcesAdapter = SourceConfigAdapter(this, coil, viewLifecycleOwner) with(binding.recyclerView) { setHasFixedSize(true) @@ -70,17 +64,22 @@ class SourcesListFragment : viewModel.items.observe(viewLifecycleOwner) { sourcesAdapter.items = it } - viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) addMenuProvider(SourcesMenuProvider()) } + override fun onResume() { + super.onResume() + activity?.setTitle(R.string.remote_sources) + } + override fun onDestroyView() { reorderHelper = null super.onDestroyView() } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, left = insets.left, right = insets.right, @@ -89,8 +88,7 @@ class SourcesListFragment : override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) { val fragment = SourceSettingsFragment.newInstance(item.source) - (parentFragment as? SettingsHeadersFragment)?.openFragment(fragment) - ?: (activity as? SettingsActivity)?.openFragment(fragment) + (activity as? SettingsActivity)?.openFragment(fragment, false) } override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt index 553988dce..5ba8cd767 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt @@ -1,26 +1,27 @@ package org.koitharu.kotatsu.settings.sources import androidx.core.os.LocaleListCompat -import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.ReversibleHandle -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.map import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.move import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.map -import org.koitharu.kotatsu.utils.ext.move import java.util.Locale import java.util.TreeMap import javax.inject.Inject @@ -35,8 +36,8 @@ class SourcesListViewModel @Inject constructor( private val settings: AppSettings, ) : BaseViewModel() { - val items = MutableLiveData>(emptyList()) - val onActionDone = SingleLiveEvent() + val items = MutableStateFlow>(emptyList()) + val onActionDone = MutableEventFlow() private val mutex = Mutex() private val expandedGroups = HashSet() @@ -49,7 +50,7 @@ class SourcesListViewModel @Inject constructor( } fun reorderSources(oldPos: Int, newPos: Int): Boolean { - val snapshot = items.value?.toMutableList() ?: return false + val snapshot = items.value.toMutableList() if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false launchAtomicJob(Dispatchers.Default) { @@ -63,10 +64,9 @@ class SourcesListViewModel @Inject constructor( } fun canReorder(oldPos: Int, newPos: Int): Boolean { - val snapshot = items.value?.toMutableList() ?: return false + val snapshot = items.value.toMutableList() if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false - if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false - return true + return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true } fun setEnabled(source: MangaSource, isEnabled: Boolean) { @@ -82,7 +82,7 @@ class SourcesListViewModel @Inject constructor( val rollback = ReversibleHandle { setEnabled(source, true) } - onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback)) + onActionDone.call(ReversibleAction(R.string.source_disabled, rollback)) } buildList() } @@ -127,21 +127,19 @@ class SourcesListViewModel @Inject constructor( val hiddenSources = settings.hiddenSources val query = searchQuery if (!query.isNullOrEmpty()) { - items.postValue( - sources.mapNotNull { - if (!it.title.contains(query, ignoreCase = true)) { - return@mapNotNull null - } - SourceConfigItem.SourceItem( - source = it, - summary = it.getLocaleTitle(), - isEnabled = it.name !in hiddenSources, - isDraggable = false, - ) - }.ifEmpty { - listOf(SourceConfigItem.EmptySearchResult) - }, - ) + items.value = sources.mapNotNull { + if (!it.title.contains(query, ignoreCase = true)) { + return@mapNotNull null + } + SourceConfigItem.SourceItem( + source = it, + summary = it.getLocaleTitle(), + isEnabled = it.name !in hiddenSources, + isDraggable = false, + ) + }.ifEmpty { + listOf(SourceConfigItem.EmptySearchResult) + } return@runInterruptible } val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) { @@ -189,7 +187,7 @@ class SourcesListViewModel @Inject constructor( } } } - items.postValue(result) + items.value = result } private fun getLocaleTitle(localeKey: String?): String? { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 1f3aa5253..bef802851 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -8,21 +8,21 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnTipCloseListener import org.koitharu.kotatsu.core.parser.favicon.faviconUri +import org.koitharu.kotatsu.core.ui.image.FaviconFallbackDrawable +import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener +import org.koitharu.kotatsu.core.util.ext.crossfade +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemExpandableBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding import org.koitharu.kotatsu.databinding.ItemTipBinding import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -import org.koitharu.kotatsu.utils.ext.crossfade -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source -import org.koitharu.kotatsu.utils.ext.textAndVisible -import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding( diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt index d8f0be9fa..d90969f17 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.settings.sources.adapter -import org.koitharu.kotatsu.base.ui.list.OnTipCloseListener +import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem interface SourceConfigListener : OnTipCloseListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt index b0f0ca321..68913fe55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -13,7 +13,6 @@ import androidx.core.view.isVisible import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.browser.BrowserCallback import org.koitharu.kotatsu.browser.BrowserClient import org.koitharu.kotatsu.browser.ProgressChromeClient @@ -21,12 +20,13 @@ import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.TaggedActivityResult +import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.TaggedActivityResult -import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability -import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat import javax.inject.Inject import com.google.android.material.R as materialR @@ -64,13 +64,13 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } - with(binding.webView.settings) { + with(viewBinding.webView.settings) { javaScriptEnabled = true userAgentString = CommonHeadersInterceptor.userAgentChrome } - binding.webView.webViewClient = BrowserClient(this) - binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(binding.webView) + viewBinding.webView.webViewClient = BrowserClient(this) + viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) onBackPressedDispatcher.addCallback(onBackPressedCallback) if (savedInstanceState != null) { return @@ -80,27 +80,27 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba source.title, getString(R.string.loading_), ) - binding.webView.loadUrl(url) + viewBinding.webView.loadUrl(url) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - binding.webView.saveState(outState) + viewBinding.webView.saveState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - binding.webView.restoreState(savedInstanceState) + viewBinding.webView.restoreState(savedInstanceState) } override fun onDestroy() { super.onDestroy() - binding.webView.destroy() + viewBinding.webView.destroy() } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { - binding.webView.stopLoading() + viewBinding.webView.stopLoading() setResult(Activity.RESULT_CANCELED) finishAfterTransition() true @@ -110,17 +110,17 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba } override fun onPause() { - binding.webView.onPause() + viewBinding.webView.onPause() super.onPause() } override fun onResume() { super.onResume() - binding.webView.onResume() + viewBinding.webView.onResume() } override fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading + viewBinding.progressBar.isVisible = isLoading if (!isLoading && authProvider.isAuthorized) { Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show() setResult(Activity.RESULT_OK) @@ -138,8 +138,8 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba } override fun onWindowInsetsChanged(insets: Insets) { - binding.appbar.updatePadding(top = insets.top) - binding.webView.updatePadding(bottom = insets.bottom) + viewBinding.appbar.updatePadding(top = insets.top) + viewBinding.webView.updatePadding(bottom = insets.bottom) } class Contract : ActivityResultContract() { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt index c7a7d4458..f0ecacc4d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt @@ -13,13 +13,14 @@ import androidx.core.view.updatePadding import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.databinding.FragmentToolsBinding import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.about.AppUpdateDialog -import org.koitharu.kotatsu.utils.ext.setChecked @AndroidEntryPoint class ToolsFragment : @@ -29,12 +30,12 @@ class ToolsFragment : private val viewModel by viewModels() - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding { return FragmentToolsBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentToolsBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.buttonSettings.setOnClickListener(this) binding.buttonDownloads.setOnClickListener(this) binding.cardUpdate.buttonChangelog.setOnClickListener(this) @@ -75,18 +76,18 @@ class ToolsFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + requireViewBinding().root.updatePadding( bottom = insets.bottom, ) } private fun onAppUpdateAvailable(version: AppVersion?) { if (version == null) { - binding.cardUpdate.root.isVisible = false + requireViewBinding().cardUpdate.root.isVisible = false return } - binding.cardUpdate.textSecondary.text = getString(R.string.new_version_s, version.name) - binding.cardUpdate.root.isVisible = true + requireViewBinding().cardUpdate.textSecondary.text = getString(R.string.new_version_s, version.name) + requireViewBinding().cardUpdate.root.isVisible = true } companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt index cbb17dd06..9d0289bb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt @@ -1,15 +1,17 @@ package org.koitharu.kotatsu.settings.tools -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus 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.core.prefs.observeAsStateFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.settings.tools.model.StorageUsage @@ -18,21 +20,18 @@ import javax.inject.Inject @HiltViewModel class ToolsViewModel @Inject constructor( private val storageManager: LocalStorageManager, - private val appUpdateRepository: AppUpdateRepository, private val settings: AppSettings, + appUpdateRepository: AppUpdateRepository, ) : BaseViewModel() { val appUpdate = appUpdateRepository.observeAvailableUpdate() - .asLiveData(viewModelScope.coroutineContext) - val storageUsage: LiveData = liveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, - ) { + val storageUsage: StateFlow = flow { emit(collectStorageUsage()) - } + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - val isIncognitoModeEnabled = settings.observeAsLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, + val isIncognitoModeEnabled = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_INCOGNITO_MODE, valueProducer = { isIncognitoModeEnabled }, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt index a6eb2bc56..0328e5534 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tools/views/MemoryUsageView.kt @@ -11,11 +11,11 @@ import androidx.core.graphics.ColorUtils import androidx.core.widget.TextViewCompat import com.google.android.material.color.MaterialColors import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView +import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.databinding.LayoutMemoryUsageBinding import org.koitharu.kotatsu.settings.tools.model.StorageUsage -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.getThemeColor class MemoryUsageView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt index e3ee6a41d..fb369ee01 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsFragment.kt @@ -20,13 +20,14 @@ import androidx.preference.MultiSelectListPreference import androidx.preference.Preference import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels +import javax.inject.Inject private const val KEY_IGNORE_DOZE = "ignore_dose" @@ -55,14 +56,13 @@ class TrackerSettingsFragment : } } } + updateDozePreference() updateCategoriesEnabled() } override fun onResume() { super.onResume() - findPreference(KEY_IGNORE_DOZE)?.run { - isVisible = isDozeIgnoreAvailable(context) - } + updateDozePreference() updateNotificationsSummary() } @@ -81,8 +81,7 @@ class TrackerSettingsFragment : when (key) { AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateNotificationsSummary() AppSettings.KEY_TRACK_SOURCES, - AppSettings.KEY_TRACKER_ENABLED, - -> updateCategoriesEnabled() + AppSettings.KEY_TRACKER_ENABLED -> updateCategoriesEnabled() } } @@ -95,28 +94,37 @@ class TrackerSettingsFragment : startActivity(intent) true } + channels.areNotificationsDisabled -> { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", requireContext().packageName, null)) startActivity(intent) true } - else -> { - super.onPreferenceTreeClick(preference) - } + + else -> super.onPreferenceTreeClick(preference) } + AppSettings.KEY_TRACK_CATEGORIES -> { TrackerCategoriesConfigSheet.show(childFragmentManager) true } + KEY_IGNORE_DOZE -> { startIgnoreDoseActivity(preference.context) true } + else -> super.onPreferenceTreeClick(preference) } } + private fun updateDozePreference() { + findPreference(KEY_IGNORE_DOZE)?.run { + isVisible = isDozeIgnoreAvailable(context) + } + } + private fun updateNotificationsSummary() { val pref = findPreference(AppSettings.KEY_NOTIFICATIONS_SETTINGS) ?: return pref.setSummary( diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt index 21c75b086..cd6713272 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/TrackerSettingsViewModel.kt @@ -1,16 +1,15 @@ package org.koitharu.kotatsu.settings.tracker -import androidx.lifecycle.MutableLiveData import androidx.room.InvalidationTracker import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import okio.Closeable -import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES import org.koitharu.kotatsu.core.db.removeObserverAsync +import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.ext.emitValue import javax.inject.Inject @HiltViewModel @@ -19,7 +18,7 @@ class TrackerSettingsViewModel @Inject constructor( private val database: MangaDatabase, ) : BaseViewModel() { - val categoriesCount = MutableLiveData(null) + val categoriesCount = MutableStateFlow(null) init { updateCategoriesCount() @@ -32,7 +31,7 @@ class TrackerSettingsViewModel @Inject constructor( private fun updateCategoriesCount() { launchJob(Dispatchers.Default) { - categoriesCount.emitValue(repository.getCategoriesCount()) + categoriesCount.value = repository.getCategoriesCount() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt index d70833062..af0ebbf85 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigAdapter.kt @@ -2,8 +2,8 @@ package org.koitharu.kotatsu.settings.tracker.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 +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener class TrackerCategoriesConfigAdapter( listener: OnListItemClickListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt index a463660aa..c9ad94181 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigSheet.kt @@ -4,33 +4,30 @@ 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.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.SheetBaseBinding @AndroidEntryPoint class TrackerCategoriesConfigSheet : - BaseBottomSheet(), - OnListItemClickListener, - View.OnClickListener { + BaseAdaptiveSheet(), + OnListItemClickListener { private val viewModel by viewModels() - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding { return SheetBaseBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: SheetBaseBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) binding.headerBar.setTitle(R.string.favourites_categories) - binding.buttonDone.isVisible = true - binding.buttonDone.setOnClickListener(this) val adapter = TrackerCategoriesConfigAdapter(this) binding.recyclerView.adapter = adapter @@ -41,10 +38,6 @@ class TrackerCategoriesConfigSheet : viewModel.toggleItem(item) } - override fun onClick(v: View?) { - dismiss() - } - companion object { private const val TAG = "TrackerCategoriesConfigSheet" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt index 1b9c10f7f..51daf79b1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoriesConfigViewModel.kt @@ -2,13 +2,15 @@ package org.koitharu.kotatsu.settings.tracker.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 kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.utils.asFlowLiveData +import javax.inject.Inject @HiltViewModel class TrackerCategoriesConfigViewModel @Inject constructor( @@ -16,7 +18,7 @@ class TrackerCategoriesConfigViewModel @Inject constructor( ) : BaseViewModel() { val content = favouritesRepository.observeCategories() - .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) private var updateJob: Job? = null diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt index 5e3876285..7db2f78b1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/tracker/categories/TrackerCategoryAD.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.settings.tracker.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.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding fun trackerCategoryAD( diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/AboutLinksPreference.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt index 0de795a3b..43e38d720 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.util.AttributeSet import androidx.preference.ListPreference -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug class ActivityListPreference : ListPreference { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt index 50e93f24b..a65122501 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextBindListener.kt @@ -2,11 +2,11 @@ package org.koitharu.kotatsu.settings.utils import android.widget.EditText import androidx.preference.EditTextPreference -import org.koitharu.kotatsu.utils.EditTextValidator +import org.koitharu.kotatsu.core.util.EditTextValidator class EditTextBindListener( private val inputType: Int, - private val hint: String, + private val hint: String?, private val validator: EditTextValidator?, ) : EditTextPreference.OnBindEditTextListener { @@ -15,4 +15,4 @@ class EditTextBindListener( editText.hint = hint validator?.attachToEditText(editText) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextDefaultSummaryProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextDefaultSummaryProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextDefaultSummaryProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextDefaultSummaryProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextSummaryProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextSummaryProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/EditTextSummaryProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/EditTextSummaryProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/LinksPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/LinksPreference.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/LinksPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/LinksPreference.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiSummaryProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/MultiSummaryProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiSummaryProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/MultiSummaryProvider.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PasswordSummaryProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PasswordSummaryProvider.kt new file mode 100644 index 000000000..5d6068aca --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/PasswordSummaryProvider.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.settings.utils + +import android.text.TextUtils +import androidx.preference.EditTextPreference +import androidx.preference.Preference + +class PasswordSummaryProvider() : Preference.SummaryProvider { + + private val delegate = EditTextPreference.SimpleSummaryProvider.getInstance() + + override fun provideSummary(preference: EditTextPreference): CharSequence? { + val summary = delegate.provideSummary(preference) + return if (summary != null && !TextUtils.isEmpty(preference.text)) { + String(CharArray(summary.length) { '\u2022' }) + } else { + summary + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt index e1f88b59c..9e2151f6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt @@ -7,7 +7,7 @@ import android.net.Uri import android.provider.Settings import androidx.activity.result.contract.ActivityResultContract import androidx.annotation.StringRes -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat class RingtonePickContract(@StringRes private val titleResId: Int) : ActivityResultContract() { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt index b2a818caa..fbf2fcc0c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/SliderPreference.kt @@ -11,7 +11,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.google.android.material.slider.Slider import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.setValueRounded +import org.koitharu.kotatsu.core.util.ext.setValueRounded class SliderPreference @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/ThemeChooserPreference.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/DomainValidator.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/DomainValidator.kt index 8e55b27c4..201fabeec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/DomainValidator.kt @@ -1,8 +1,8 @@ -package org.koitharu.kotatsu.settings +package org.koitharu.kotatsu.settings.utils.validation import okhttp3.HttpUrl import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.EditTextValidator +import org.koitharu.kotatsu.core.util.EditTextValidator class DomainValidator : EditTextValidator() { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HeaderValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/HeaderValidator.kt similarity index 85% rename from app/src/main/java/org/koitharu/kotatsu/settings/HeaderValidator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/HeaderValidator.kt index 9e251c0a7..36891f980 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HeaderValidator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/HeaderValidator.kt @@ -1,9 +1,9 @@ -package org.koitharu.kotatsu.settings +package org.koitharu.kotatsu.settings.utils.validation import okhttp3.Headers import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.network.CommonHeaders -import org.koitharu.kotatsu.utils.EditTextValidator +import org.koitharu.kotatsu.core.util.EditTextValidator class HeaderValidator : EditTextValidator() { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/PortNumberValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/PortNumberValidator.kt new file mode 100644 index 000000000..3bee9f9a5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/validation/PortNumberValidator.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.settings.utils.validation + +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.EditTextValidator + +class PortNumberValidator : EditTextValidator() { + + override fun validate(text: String): ValidationResult { + val trimmed = text.trim() + if (trimmed.isEmpty()) { + return ValidationResult.Success + } + return if (!checkCharacters(trimmed)) { + ValidationResult.Failed(context.getString(R.string.invalid_port_number)) + } else { + ValidationResult.Success + } + } + + private fun checkCharacters(value: String): Boolean { + val intValue = value.toIntOrNull() ?: return false + return intValue in 1..65535 + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt index d6e9a7fac..b2104c199 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt @@ -1,9 +1,5 @@ package org.koitharu.kotatsu.shelf.domain -import dagger.Reusable -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.combine @@ -18,19 +14,19 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.favourites.data.toMangaList -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.shelf.domain.model.ShelfContent import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import javax.inject.Inject -@Reusable -class ShelfRepository @Inject constructor( +@Suppress("SameParameterValue") +class ShelfContentObserveUseCase @Inject constructor( private val localMangaRepository: LocalMangaRepository, private val historyRepository: HistoryRepository, private val trackingRepository: TrackingRepository, @@ -39,21 +35,21 @@ class ShelfRepository @Inject constructor( @LocalStorageChanges private val localStorageChanges: SharedFlow, ) { - fun observeShelfContent(): Flow = combine( - historyRepository.observeAllWithHistory(), - observeLocalManga(SortOrder.UPDATED), + operator fun invoke(): Flow = combine( + historyRepository.observeAll(20), + observeLocalManga(SortOrder.UPDATED, 20), observeFavourites(), trackingRepository.observeUpdatedManga(), - suggestionRepository.observeAll(16), + suggestionRepository.observeAll(20), ) { history, local, favorites, updated, suggestions -> ShelfContent(history, favorites, updated, local, suggestions) } - private fun observeLocalManga(sortOrder: SortOrder): Flow> { + private fun observeLocalManga(sortOrder: SortOrder, limit: Int): Flow> { return localStorageChanges .onStart { emit(null) } .mapLatest { - localMangaRepository.getList(0, null, sortOrder) + localMangaRepository.getList(0, null, sortOrder).take(limit) }.distinctUntilChanged() } @@ -69,23 +65,6 @@ class ShelfRepository @Inject constructor( } } - suspend fun deleteLocalManga(ids: Set) { - val list = localMangaRepository.getList(0, null, null) - .filter { x -> x.id in ids } - coroutineScope { - list.map { manga -> - async { - val original = localMangaRepository.getRemoteManga(manga) - if (localMangaRepository.delete(manga)) { - runCatchingCancellable { - historyRepository.deleteOrSwap(manga, original) - } - } - } - }.awaitAll() - } - } - private fun observeCategoriesContent( categories: List, ) = combine>, Map>>( diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt index 1319cf991..39c88010f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt @@ -1,13 +1,12 @@ -package org.koitharu.kotatsu.shelf.domain +package org.koitharu.kotatsu.shelf.domain.model 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, + val history: List, val favourites: Map>, - val updated: Map, + val updated: List, val local: List, val suggestions: List, ) { diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt similarity index 62% rename from app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt index d798c09e5..ecdd14b7d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.domain +package org.koitharu.kotatsu.shelf.domain.model enum class ShelfSection { diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt index 0cf72a0a9..50e2073af 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt @@ -13,13 +13,16 @@ import androidx.fragment.app.viewModels import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner -import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentShelfBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver @@ -35,7 +38,6 @@ import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity -import org.koitharu.kotatsu.utils.ext.addMenuProvider import javax.inject.Inject @AndroidEntryPoint @@ -56,14 +58,14 @@ class ShelfFragment : private var selectionController: SectionedSelectionController? = null override val recyclerView: RecyclerView - get() = binding.recyclerView + get() = requireViewBinding().recyclerView - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentShelfBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentShelfBinding { return FragmentShelfBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentShelfBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) nestedScrollStateHandle = NestedScrollStateHandle(savedInstanceState, KEY_NESTED_SCROLL) val sizeResolver = ItemSizeResolver(resources, settings) selectionController = SectionedSelectionController( @@ -81,12 +83,12 @@ class ShelfFragment : ) binding.recyclerView.adapter = adapter binding.recyclerView.setHasFixedSize(true) - addMenuProvider(ShelfMenuProvider(view.context, childFragmentManager, viewModel)) + addMenuProvider(ShelfMenuProvider(binding.root.context, childFragmentManager, viewModel)) viewModel.content.observe(viewLifecycleOwner, ::onListChanged) - viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) - viewModel.onDownloadStarted.observe(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) + viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) } override fun onSaveInstanceState(outState: Bundle) { @@ -136,7 +138,7 @@ class ShelfFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt index d00601406..842699dfc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt @@ -8,11 +8,11 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener +import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener +import org.koitharu.kotatsu.core.util.ext.startOfDay 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 java.util.Date import java.util.concurrent.TimeUnit import com.google.android.material.R as materialR diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt index 4dfa746ee..204dd7599 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt @@ -8,15 +8,15 @@ import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration -import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.util.ShareHelper +import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations +import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.flattenTo import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations class ShelfSelectionCallback( private val recyclerView: RecyclerView, @@ -63,7 +63,7 @@ class ShelfSelectionCallback( } R.id.action_favourite -> { - FavouriteCategoriesBottomSheet.show(fragmentManager, collectSelectedItems(controller)) + FavouriteCategoriesSheet.show(fragmentManager, collectSelectedItems(controller)) mode.finish() true } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt similarity index 71% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 2103b208f..16ea86002 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -1,25 +1,28 @@ package org.koitharu.kotatsu.shelf.ui import androidx.collection.ArraySet -import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.MangaWithHistory -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -28,44 +31,43 @@ 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.local.domain.DeleteLocalMangaUseCase 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.domain.ShelfContentObserveUseCase +import org.koitharu.kotatsu.shelf.domain.model.ShelfContent +import org.koitharu.kotatsu.shelf.domain.model.ShelfSection import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.sync.domain.SyncController -import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel class ShelfViewModel @Inject constructor( - private val repository: ShelfRepository, private val historyRepository: HistoryRepository, private val favouritesRepository: FavouritesRepository, - private val trackingRepository: TrackingRepository, private val settings: AppSettings, private val downloadScheduler: DownloadWorker.Scheduler, + private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, + private val listExtraProvider: ListExtraProvider, + shelfContentObserveUseCase: ShelfContentObserveUseCase, syncController: SyncController, networkState: NetworkState, -) : BaseViewModel(), ListExtraProvider { +) : BaseViewModel() { - val onActionDone = SingleLiveEvent() - val onDownloadStarted = SingleLiveEvent() + val onActionDone = MutableEventFlow() + val onDownloadStarted = MutableEventFlow() - val content: LiveData> = combine( + val content: StateFlow> = combine( settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections }, settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }, settings.observeAsFlow(AppSettings.KEY_SUGGESTIONS) { isSuggestionsEnabled }, networkState, - repository.observeShelfContent(), + shelfContentObserveUseCase(), ) { sections, isTrackerEnabled, isSuggestionsEnabled, isConnected, content -> mapList(content, isTrackerEnabled, isSuggestionsEnabled, sections, isConnected) }.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { launchJob(Dispatchers.Default) { @@ -73,29 +75,13 @@ class ShelfViewModel @Inject constructor( } } - override suspend fun getCounter(mangaId: Long): Int { - return if (settings.isTrackerEnabled) { - trackingRepository.getNewChaptersCount(mangaId) - } else { - 0 - } - } - - override suspend fun getProgress(mangaId: Long): Float { - return if (settings.isReadingIndicatorsEnabled) { - historyRepository.getProgress(mangaId) - } else { - PROGRESS_NONE - } - } - fun removeFromFavourites(category: FavouriteCategory, ids: Set) { if (ids.isEmpty()) { return } launchJob(Dispatchers.Default) { val handle = favouritesRepository.removeFromCategory(category.id, ids) - onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle)) + onActionDone.call(ReversibleAction(R.string.removed_from_favourites, handle)) } } @@ -105,14 +91,14 @@ class ShelfViewModel @Inject constructor( } launchJob(Dispatchers.Default) { val handle = historyRepository.delete(ids) - onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle)) + onActionDone.call(ReversibleAction(R.string.removed_from_history, handle)) } } fun deleteLocal(ids: Set) { launchLoadingJob(Dispatchers.Default) { - repository.deleteLocalManga(ids) - onActionDone.emitCall(ReversibleAction(R.string.removal_completed, null)) + deleteLocalMangaUseCase(ids) + onActionDone.call(ReversibleAction(R.string.removal_completed, null)) } } @@ -125,12 +111,12 @@ class ShelfViewModel @Inject constructor( historyRepository.deleteAfter(minDate) R.string.removed_from_history } - onActionDone.emitCall(ReversibleAction(stringRes, null)) + onActionDone.call(ReversibleAction(stringRes, null)) } } fun getManga(ids: Set): Set { - val snapshot = content.value ?: return emptySet() + val snapshot = content.value val result = ArraySet(ids.size) for (section in snapshot) { if (section !is ShelfSectionModel) { @@ -151,7 +137,7 @@ class ShelfViewModel @Inject constructor( fun download(items: Set) { launchJob(Dispatchers.Default) { downloadScheduler.schedule(items) - onDownloadStarted.emitCall(Unit) + onDownloadStarted.call(Unit) } } @@ -189,7 +175,7 @@ class ShelfViewModel @Inject constructor( when (section) { ShelfSection.HISTORY -> mapHistory( result, - content.history.filter { it.manga.source == MangaSource.LOCAL }, + content.history.filter { it.source == MangaSource.LOCAL }, ) ShelfSection.LOCAL -> mapLocal(result, content.local) @@ -217,17 +203,14 @@ class ShelfViewModel @Inject constructor( private suspend fun mapHistory( destination: MutableList, - list: List, + list: List, ) { if (list.isEmpty()) { return } - val showPercent = settings.isReadingIndicatorsEnabled destination += ShelfSectionModel.History( - items = list.map { (manga, history) -> - val counter = getCounter(manga.id) - val percent = if (showPercent) history.percent else PROGRESS_NONE - manga.toGridModel(counter, percent) + items = list.map { manga -> + manga.toGridModel(listExtraProvider) }, showAllButtonText = R.string.show_all, ) @@ -235,16 +218,15 @@ class ShelfViewModel @Inject constructor( private suspend fun mapUpdated( destination: MutableList, - updated: Map, + updated: List, ) { if (updated.isEmpty()) { return } - val showPercent = settings.isReadingIndicatorsEnabled + settings.isReadingIndicatorsEnabled destination += ShelfSectionModel.Updated( - items = updated.map { (manga, counter) -> - val percent = if (showPercent) getProgress(manga.id) else PROGRESS_NONE - manga.toGridModel(counter, percent) + items = updated.map { manga -> + manga.toGridModel(listExtraProvider) }, showAllButtonText = R.string.show_all, ) @@ -258,7 +240,7 @@ class ShelfViewModel @Inject constructor( return } destination += ShelfSectionModel.Local( - items = local.toUi(ListMode.GRID, this, null), + items = local.toUi(ListMode.GRID, listExtraProvider), showAllButtonText = R.string.show_all, ) } @@ -271,7 +253,7 @@ class ShelfViewModel @Inject constructor( return } destination += ShelfSectionModel.Suggestions( - items = suggestions.toUi(ListMode.GRID, this, null), + items = suggestions.toUi(ListMode.GRID, listExtraProvider), showAllButtonText = R.string.show_all, ) } @@ -286,7 +268,7 @@ class ShelfViewModel @Inject constructor( for ((category, list) in favourites) { if (list.isNotEmpty()) { destination += ShelfSectionModel.Favourites( - items = list.toUi(ListMode.GRID, this, null), + items = list.toUi(ListMode.GRID, listExtraProvider), category = category, showAllButtonText = R.string.show_all, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/MangaItemDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/MangaItemDiffCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/MangaItemDiffCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/MangaItemDiffCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt index e96a9735f..85baded5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt @@ -6,9 +6,9 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD @@ -16,6 +16,7 @@ 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.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import kotlin.jvm.internal.Intrinsics @@ -62,6 +63,10 @@ class ShelfAdapter( oldItem.key == newItem.key } + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + else -> oldItem.javaClass == newItem.javaClass } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt index bf2c3e0fe..c0e561475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt @@ -7,19 +7,19 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.NestedScrollStateHandle -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController -import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration -import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController +import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.util.ext.removeItemDecoration +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.ItemListGroupBinding import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel -import org.koitharu.kotatsu.utils.ext.removeItemDecoration -import org.koitharu.kotatsu.utils.ext.setTextAndVisible fun shelfGroupAD( sharedPool: RecyclerView.RecycledViewPool, diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt index c4dce1804..e8c8f346f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt @@ -12,7 +12,8 @@ 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.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding import com.google.android.material.R as materialR @@ -31,17 +32,15 @@ class ShelfSettingsActivity : setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } - binding.buttonDone.setOnClickListener(this) + viewBinding.buttonDone.setOnClickListener(this) val settingsAdapter = ShelfSettingsAdapter(this) - with(binding.recyclerView) { + with(viewBinding.recyclerView) { setHasFixedSize(true) adapter = settingsAdapter reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also { it.attachToRecyclerView(this) } } - - viewModel.content.observe(this) { settingsAdapter.items = it } } @@ -58,14 +57,14 @@ class ShelfSettingsActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( bottom = insets.bottom, ) - binding.toolbar.updateLayoutParams { + viewBinding.toolbar.updateLayoutParams { topMargin = insets.top } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt index 4b73dc9ef..736aa125f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt @@ -7,10 +7,10 @@ import android.widget.CompoundButton import androidx.core.view.updatePaddingRelative import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding -import org.koitharu.kotatsu.shelf.domain.ShelfSection -import org.koitharu.kotatsu.utils.ext.setChecked +import org.koitharu.kotatsu.shelf.domain.model.ShelfSection @SuppressLint("ClickableViewAccessibility") fun shelfSectionAD( diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt index e75f329de..c45d6bee3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt @@ -1,7 +1,7 @@ package org.koitharu.kotatsu.shelf.ui.config import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.shelf.domain.ShelfSection +import org.koitharu.kotatsu.shelf.domain.model.ShelfSection sealed interface ShelfSettingsItemModel : ListModel { @@ -19,9 +19,7 @@ sealed interface ShelfSettingsItemModel : ListModel { other as Section if (section != other.section) return false - if (isChecked != other.isChecked) return false - - return true + return isChecked == other.isChecked } override fun hashCode(): Int { @@ -45,9 +43,7 @@ sealed interface ShelfSettingsItemModel : ListModel { if (id != other.id) return false if (title != other.title) return false - if (isChecked != other.isChecked) return false - - return true + return isChecked == other.isChecked } override fun hashCode(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt index fb09af8b1..4df55a1d1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt @@ -4,15 +4,17 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus 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.core.ui.BaseViewModel 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 org.koitharu.kotatsu.parsers.util.move +import org.koitharu.kotatsu.shelf.domain.model.ShelfSection import javax.inject.Inject @HiltViewModel @@ -27,7 +29,7 @@ class ShelfSettingsViewModel @Inject constructor( favouritesRepository.observeCategories(), ) { sections, isTrackerEnabled, categories -> buildList(sections, isTrackerEnabled, categories) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) private var updateJob: Job? = null @@ -57,7 +59,7 @@ class ShelfSettingsViewModel @Inject constructor( } fun reorderSections(oldPos: Int, newPos: Int): Boolean { - val snapshot = content.value?.toMutableList() ?: return false + val snapshot = content.value.toMutableList() snapshot.move(oldPos, newPos) settings.shelfSections = snapshot.sections() return true diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt similarity index 71% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt index 9677f8d4e..26f74d59b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt @@ -9,11 +9,11 @@ import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.Slider import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.util.ext.setValueRounded +import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding -import org.koitharu.kotatsu.utils.ext.setValueRounded -import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter import javax.inject.Inject @AndroidEntryPoint @@ -26,13 +26,13 @@ class ShelfSizeBottomSheet : lateinit var settings: AppSettings private var labelFormatter: LabelFormatter? = null - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetShelfSizeBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetShelfSizeBinding { return SheetShelfSizeBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - labelFormatter = IntPercentLabelFormatter(view.context) + override fun onViewBindingCreated(binding: SheetShelfSizeBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + labelFormatter = IntPercentLabelFormatter(binding.root.context) binding.sliderGrid.addOnChangeListener(this) binding.buttonSmall.setOnClickListener(this) binding.buttonLarge.setOnClickListener(this) @@ -47,11 +47,11 @@ class ShelfSizeBottomSheet : override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { settings.gridSize = value.toInt() - binding.textViewLabel.text = labelFormatter?.getFormattedValue(value) + requireViewBinding().textViewLabel.text = labelFormatter?.getFormattedValue(value) } override fun onClick(v: View) { - val slider = binding.sliderGrid + val slider = requireViewBinding().sliderGrid when (v.id) { R.id.button_small -> slider.setValueRounded(slider.value - slider.stepSize) R.id.button_large -> slider.setValueRounded(slider.value + slider.stepSize) diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionWithManga.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/MangaSuggestion.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index 14c4561d8..873d7750e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -7,9 +7,9 @@ import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.suggestions.data.SuggestionEntity -import org.koitharu.kotatsu.utils.ext.mapItems import javax.inject.Inject class SuggestionRepository @Inject constructor( diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt index 691928fa8..a408dd722 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt @@ -1,8 +1,8 @@ package org.koitharu.kotatsu.suggestions.domain +import org.koitharu.kotatsu.core.util.ext.almostEquals import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.almostEquals class TagsBlacklist( private val tags: Set, diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt index 4e225378d..9c38cf50c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsActivity.kt @@ -9,10 +9,9 @@ import androidx.fragment.app.commit import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner -import kotlin.text.Typography.dagger @AndroidEntryPoint class SuggestionsActivity : @@ -20,7 +19,7 @@ class SuggestionsActivity : AppBarOwner { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -37,7 +36,7 @@ class SuggestionsActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt similarity index 81% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index a1c8ad796..17fdb6830 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -4,24 +4,24 @@ import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View import androidx.appcompat.view.ActionMode import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.ui.list.ListSelectionController +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.settings.SettingsActivity -import org.koitharu.kotatsu.utils.ext.addMenuProvider class SuggestionsFragment : MangaListFragment() { override val viewModel by viewModels() override val isSwipeRefreshEnabled = false - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(SuggestionMenuProvider()) } @@ -42,16 +42,18 @@ class SuggestionsFragment : MangaListFragment() { R.id.action_update -> { SuggestionsWorker.startNow(requireContext()) Snackbar.make( - binding.recyclerView, + requireViewBinding().recyclerView, R.string.feed_will_update_soon, Snackbar.LENGTH_LONG, ).show() true } + R.id.action_settings -> { startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext())) true } + else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index bbb548e77..40338a217 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -3,34 +3,36 @@ package org.koitharu.kotatsu.suggestions.ui import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.onFirst import javax.inject.Inject @HiltViewModel class SuggestionsViewModel @Inject constructor( repository: SuggestionRepository, settings: AppSettings, - private val tagHighlighter: MangaTagHighlighter, + private val extraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { override val content = combine( repository.observeAll(), - listModeFlow, + listMode, ) { list, mode -> when { list.isEmpty() -> listOf( @@ -42,7 +44,7 @@ class SuggestionsViewModel @Inject constructor( ), ) - else -> list.toUi(mode, tagHighlighter) + else -> list.toUi(mode, extraProvider) } }.onStart { loadingCounter.increment() @@ -50,10 +52,7 @@ class SuggestionsViewModel @Inject constructor( loadingCounter.decrement() }.catch { emit(listOf(it.toErrorState(canRetry = false))) - }.asFlowLiveData( - viewModelScope.coroutineContext + Dispatchers.Default, - listOf(LoadingState), - ) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) override fun onRefresh() = Unit diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 23f1f2766..f4b6a24fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -38,25 +38,26 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.almostEquals +import org.koitharu.kotatsu.core.util.ext.asArrayList +import org.koitharu.kotatsu.core.util.ext.flatten +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.sanitize +import org.koitharu.kotatsu.core.util.ext.takeMostFrequent +import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull +import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist -import org.koitharu.kotatsu.utils.ext.almostEquals -import org.koitharu.kotatsu.utils.ext.asArrayList -import org.koitharu.kotatsu.utils.ext.flatten -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.takeMostFrequent -import org.koitharu.kotatsu.utils.ext.toBitmapOrNull -import org.koitharu.kotatsu.utils.ext.trySetForeground import java.util.concurrent.TimeUnit import kotlin.math.pow import kotlin.random.Random @@ -74,6 +75,10 @@ class SuggestionsWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { + if (!appSettings.isSuggestionsEnabled) { + suggestionRepository.clear() + return Result.success() + } trySetForeground() val count = doWorkImpl() val outputData = workDataOf(DATA_COUNT to count) @@ -112,10 +117,6 @@ class SuggestionsWorker @AssistedInject constructor( } private suspend fun doWorkImpl(): Int { - if (!appSettings.isSuggestionsEnabled) { - suggestionRepository.clear() - return 0 - } val seed = ( historyRepository.getList(0, 20) + favouritesRepository.getLastManga(20) @@ -227,7 +228,7 @@ class SuggestionsWorker @AssistedInject constructor( ).toBitmapOrNull(), ) setSmallIcon(R.drawable.ic_stat_suggestion) - val description = manga.description?.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT) + val description = manga.description?.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT)?.sanitize() if (!description.isNullOrBlank()) { val style = NotificationCompat.BigTextStyle() style.bigText( @@ -262,7 +263,7 @@ class SuggestionsWorker @AssistedInject constructor( PendingIntentCompat.getActivity( applicationContext, id + 2, - ReaderActivity.newIntent(applicationContext, manga), + IntentBuilder(applicationContext).manga(manga).build(), 0, false, ), diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt index a3792664f..b9f6b6683 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt @@ -5,15 +5,16 @@ import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject import org.koitharu.kotatsu.core.exceptions.SyncApiException +import org.koitharu.kotatsu.core.network.BaseHttpClient +import org.koitharu.kotatsu.core.util.ext.toRequestBody import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.parseJson import org.koitharu.kotatsu.parsers.util.removeSurrounding -import org.koitharu.kotatsu.utils.ext.toRequestBody import javax.inject.Inject @Reusable class SyncAuthApi @Inject constructor( - private val okHttpClient: OkHttpClient, + @BaseHttpClient private val okHttpClient: OkHttpClient, ) { suspend fun authenticate(host: String, email: String, password: String): String { diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt index cee53bdb0..7e211e3d4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt @@ -7,7 +7,7 @@ import androidx.annotation.WorkerThread import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import javax.inject.Inject @Reusable diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncController.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncController.kt index 32cd68134..ba00c6285 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncController.kt @@ -23,7 +23,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES import org.koitharu.kotatsu.core.db.TABLE_HISTORY -import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Provider diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncHelper.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncHelper.kt index 089cb6a9c..e8c740c1b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncHelper.kt @@ -26,15 +26,15 @@ import org.koitharu.kotatsu.core.db.TABLE_TAGS import org.koitharu.kotatsu.core.logs.LoggersModule import org.koitharu.kotatsu.core.network.GZipInterceptor import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull +import org.koitharu.kotatsu.core.util.ext.toContentValues +import org.koitharu.kotatsu.core.util.ext.toJson +import org.koitharu.kotatsu.core.util.ext.toRequestBody import org.koitharu.kotatsu.parsers.util.json.mapJSONTo import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthenticator import org.koitharu.kotatsu.sync.data.SyncInterceptor import org.koitharu.kotatsu.sync.data.SyncSettings -import org.koitharu.kotatsu.utils.ext.parseJsonOrNull -import org.koitharu.kotatsu.utils.ext.toContentValues -import org.koitharu.kotatsu.utils.ext.toJson -import org.koitharu.kotatsu.utils.ext.toRequestBody import java.util.concurrent.TimeUnit private const val FIELD_TIMESTAMP = "timestamp" diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt index b4bf3a2a6..a7cd65491 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthActivity.kt @@ -19,12 +19,14 @@ import androidx.transition.TransitionManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivitySyncAuthBinding import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.sync.domain.SyncAuthResult -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat @AndroidEntryPoint class SyncAuthActivity : BaseActivity(), View.OnClickListener, FragmentResultListener { @@ -41,21 +43,21 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi accountAuthenticatorResponse = intent.getParcelableExtraCompat(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) accountAuthenticatorResponse?.onRequestContinued() - binding.buttonCancel.setOnClickListener(this) - binding.buttonNext.setOnClickListener(this) - binding.buttonBack.setOnClickListener(this) - binding.buttonDone.setOnClickListener(this) - binding.layoutProgress.setOnClickListener(this) - binding.buttonSettings.setOnClickListener(this) - binding.editEmail.addTextChangedListener(EmailTextWatcher(binding.buttonNext)) - binding.editPassword.addTextChangedListener(PasswordTextWatcher(binding.buttonDone)) + viewBinding.buttonCancel.setOnClickListener(this) + viewBinding.buttonNext.setOnClickListener(this) + viewBinding.buttonBack.setOnClickListener(this) + viewBinding.buttonDone.setOnClickListener(this) + viewBinding.layoutProgress.setOnClickListener(this) + viewBinding.buttonSettings.setOnClickListener(this) + viewBinding.editEmail.addTextChangedListener(EmailTextWatcher(viewBinding.buttonNext)) + viewBinding.editPassword.addTextChangedListener(PasswordTextWatcher(viewBinding.buttonDone)) onBackPressedDispatcher.addCallback(pageBackCallback) - viewModel.onTokenObtained.observe(this, ::onTokenReceived) - viewModel.onError.observe(this, ::onError) + viewModel.onTokenObtained.observeEvent(this, ::onTokenReceived) + viewModel.onError.observeEvent(this, ::onError) viewModel.isLoading.observe(this, ::onLoadingStateChanged) - viewModel.onAccountAlreadyExists.observe(this) { + viewModel.onAccountAlreadyExists.observeEvent(this) { onAccountAlreadyExists() } @@ -65,7 +67,7 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi override fun onWindowInsetsChanged(insets: Insets) { val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding) - binding.root.setPadding( + viewBinding.root.setPadding( basePadding + insets.left, basePadding + insets.top, basePadding + insets.right, @@ -81,23 +83,23 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi } R.id.button_next -> { - binding.groupLogin.isVisible = false - binding.groupPassword.isVisible = true + viewBinding.groupLogin.isVisible = false + viewBinding.groupPassword.isVisible = true pageBackCallback.update() - binding.editPassword.requestFocus() + viewBinding.editPassword.requestFocus() } R.id.button_back -> { - binding.groupPassword.isVisible = false - binding.groupLogin.isVisible = true + viewBinding.groupPassword.isVisible = false + viewBinding.groupLogin.isVisible = true pageBackCallback.update() - binding.editEmail.requestFocus() + viewBinding.editEmail.requestFocus() } R.id.button_done -> { viewModel.obtainToken( - email = binding.editEmail.text.toString(), - password = binding.editPassword.text.toString(), + email = viewBinding.editEmail.text.toString(), + password = viewBinding.editPassword.text.toString(), ) } @@ -122,11 +124,11 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi } private fun onLoadingStateChanged(isLoading: Boolean) { - if (isLoading == binding.layoutProgress.isVisible) { + if (isLoading == viewBinding.layoutProgress.isVisible) { return } - TransitionManager.beginDelayedTransition(binding.root, Fade()) - binding.layoutProgress.isVisible = isLoading + TransitionManager.beginDelayedTransition(viewBinding.root, Fade()) + viewBinding.layoutProgress.isVisible = isLoading pageBackCallback.update() } @@ -200,14 +202,14 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi private inner class PageBackCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - binding.groupLogin.isVisible = true - binding.groupPassword.isVisible = false - binding.editEmail.requestFocus() + viewBinding.groupLogin.isVisible = true + viewBinding.groupPassword.isVisible = false + viewBinding.editEmail.requestFocus() update() } fun update() { - isEnabled = !binding.layoutProgress.isVisible && binding.groupPassword.isVisible + isEnabled = !viewBinding.layoutProgress.isVisible && viewBinding.groupPassword.isVisible } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt similarity index 66% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt index d86bc3c8d..fd02134fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt @@ -2,16 +2,17 @@ package org.koitharu.kotatsu.sync.ui import android.accounts.AccountManager import android.content.Context -import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.domain.SyncAuthResult -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty import javax.inject.Inject @HiltViewModel @@ -20,9 +21,9 @@ class SyncAuthViewModel @Inject constructor( private val api: SyncAuthApi, ) : BaseViewModel() { - val onAccountAlreadyExists = SingleLiveEvent() - val onTokenObtained = SingleLiveEvent() - val host = MutableLiveData("") + val onAccountAlreadyExists = MutableEventFlow() + val onTokenObtained = MutableEventFlow() + val host = MutableStateFlow("") private val defaultHost = context.getString(R.string.sync_host_default) @@ -31,7 +32,7 @@ class SyncAuthViewModel @Inject constructor( val am = AccountManager.get(context) val accounts = am.getAccountsByType(context.getString(R.string.account_type_sync)) if (accounts.isNotEmpty()) { - onAccountAlreadyExists.emitCall(Unit) + onAccountAlreadyExists.call(Unit) } } } @@ -40,8 +41,8 @@ class SyncAuthViewModel @Inject constructor( val hostValue = host.value.ifNullOrEmpty { defaultHost } launchLoadingJob(Dispatchers.Default) { val token = api.authenticate(hostValue, email, password) - val result = SyncAuthResult(host.value.orEmpty(), email, password, token) - onTokenObtained.emitCall(result) + val result = SyncAuthResult(host.value, email, password, token) + onTokenObtained.call(result) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt similarity index 75% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt index 970c33938..2de3183f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.sync.ui import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.widget.ArrayAdapter @@ -13,9 +12,9 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding -import org.koitharu.kotatsu.settings.DomainValidator +import org.koitharu.kotatsu.settings.utils.validation.DomainValidator import org.koitharu.kotatsu.sync.data.SyncSettings import javax.inject.Inject @@ -26,7 +25,7 @@ class SyncHostDialogFragment : AlertDialogFragment { - topMargin = view.resources.getDimensionPixelOffset(R.dimen.screen_padding) + topMargin = binding.root.resources.getDimensionPixelOffset(R.dimen.screen_padding) bottomMargin = topMargin } binding.message.setText(R.string.sync_host_description) - val entries = view.resources.getStringArray(R.array.sync_host_list) + val entries = binding.root.resources.getStringArray(R.array.sync_host_list) val editText = binding.edit editText.setText(syncSettings.host) editText.threshold = 0 - editText.setAdapter(ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, entries)) + editText.setAdapter(ArrayAdapter(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries)) binding.dropdown.setOnClickListener { editText.showDropDown() } @@ -60,7 +62,7 @@ class SyncHostDialogFragment : AlertDialogFragment { - val result = binding.edit.text?.toString().orEmpty() + val result = requireViewBinding().edit.text?.toString().orEmpty() syncSettings.host = result parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_HOST to result)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncSettingsIntent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncSettingsIntent.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncSettingsIntent.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncSettingsIntent.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt index ff0157857..cb9dcecca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt @@ -7,10 +7,10 @@ import android.content.Context import android.content.SyncResult import android.os.Bundle import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.onError +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncHelper -import org.koitharu.kotatsu.utils.ext.onError -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncService.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt index 9297ea5fc..279724dd7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt @@ -7,10 +7,10 @@ import android.content.Context import android.content.SyncResult import android.os.Bundle import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.onError +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncHelper -import org.koitharu.kotatsu.utils.ext.onError -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncService.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/history/HistorySyncService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/EntityMapping.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/data/EntityMapping.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/EntityMapping.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackLogEntity.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackLogWithManga.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 0915582a1..bd9b03a83 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -34,9 +34,8 @@ abstract class TracksDao { abstract fun observeNewChapters(mangaId: Long): Flow @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> + @Query("SELECT manga.* 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> @Query("DELETE FROM tracks") abstract suspend fun clear() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt index 31df0a6be..fcc8f996a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -1,16 +1,16 @@ package org.koitharu.kotatsu.tracker.domain import androidx.annotation.VisibleForTesting -import javax.inject.Inject import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackingItem +import javax.inject.Inject class Tracker @Inject constructor( private val settings: AppSettings, @@ -114,9 +114,11 @@ class Tracker @Inject constructor( newChapters.isEmpty() -> { MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) } + newChapters.size == chapters.size -> { MangaUpdates(manga, emptyList(), isValid = false) } + else -> { MangaUpdates(manga, newChapters, isValid = true) } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index a6b3b8518..482b25fa1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource @@ -44,9 +45,9 @@ class TrackingRepository @Inject constructor( return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } } } - fun observeUpdatedManga(): Flow> { + fun observeUpdatedManga(): Flow> { return db.tracksDao.observeUpdatedManga() - .map { x -> x.mapKeys { it.key.toManga() } } + .mapItems { it.toManga() } .distinctUntilChanged() } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaTracking.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/TrackingLogItem.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index ac4d538af..588e22677 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -12,10 +12,14 @@ import coil.ImageLoader import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener -import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver +import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener +import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration +import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -26,8 +30,6 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker -import org.koitharu.kotatsu.utils.ext.addMenuProvider -import org.koitharu.kotatsu.utils.ext.getThemeColor import javax.inject.Inject @AndroidEntryPoint @@ -43,13 +45,13 @@ class FeedFragment : private var feedAdapter: FeedAdapter? = null - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentFeedBinding.inflate(inflater, container, false) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this) with(binding.recyclerView) { adapter = feedAdapter @@ -76,11 +78,11 @@ class FeedFragment : ) viewModel.content.observe(viewLifecycleOwner, this::onListChanged) - viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) - viewModel.onFeedCleared.observe(viewLifecycleOwner) { + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { onFeedCleared() } - TrackWorker.getIsRunningLiveData(view.context.applicationContext) + TrackWorker.observeIsRunning(binding.root.context.applicationContext) .observe(viewLifecycleOwner, this::onIsTrackerRunningChanged) } @@ -90,7 +92,7 @@ class FeedFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) } @@ -115,7 +117,7 @@ class FeedFragment : private fun onFeedCleared() { val snackbar = Snackbar.make( - binding.recyclerView, + requireViewBinding().recyclerView, R.string.updates_feed_cleared, Snackbar.LENGTH_LONG, ) @@ -124,7 +126,7 @@ class FeedFragment : } private fun onIsTrackerRunningChanged(isRunning: Boolean) { - binding.swipeRefreshLayout.isRefreshing = isRunning + requireViewBinding().swipeRefreshLayout.isRefreshing = isRunning } override fun onScrolledToEnd() { diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt index 3f154324c..88d888039 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt @@ -7,7 +7,7 @@ import android.view.MenuItem import android.view.View import androidx.core.view.MenuProvider import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog +import org.koitharu.kotatsu.core.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.tracker.work.TrackWorker diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt index 2a5c6bcc5..ce1c89b3a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt @@ -4,19 +4,22 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.daysDiff 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.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem -import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.daysDiff import java.util.Date import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -32,7 +35,7 @@ class FeedViewModel @Inject constructor( private val limit = MutableStateFlow(PAGE_SIZE) private val isReady = AtomicBoolean(false) - val onFeedCleared = SingleLiveEvent() + val onFeedCleared = MutableEventFlow() val content = repository.observeTrackingLog(limit) .map { list -> if (list.isEmpty()) { @@ -48,7 +51,7 @@ class FeedViewModel @Inject constructor( isReady.set(true) list.mapList() } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) fun clearFeed(clearCounters: Boolean) { launchLoadingJob(Dispatchers.Default) { @@ -56,7 +59,7 @@ class FeedViewModel @Inject constructor( if (clearCounters) { repository.clearCounters() } - onFeedCleared.emitCall(Unit) + onFeedCleared.call(Unit) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt similarity index 76% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt index b35158c6e..063e17d8c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt @@ -1,10 +1,12 @@ package org.koitharu.kotatsu.tracker.ui.feed.adapter +import android.content.Context import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.core.ui.DateTimeAgo +import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller +import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD @@ -13,6 +15,7 @@ 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.LoadingFooter import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem import kotlin.jvm.internal.Intrinsics @@ -20,7 +23,7 @@ class FeedAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, listener: MangaListListener, -) : AsyncListDifferDelegationAdapter(DiffCallback()) { +) : AsyncListDifferDelegationAdapter(DiffCallback()), FastScroller.SectionIndexer { init { delegatesManager @@ -33,6 +36,17 @@ class FeedAdapter( .addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD()) } + override fun getSectionText(context: Context, position: Int): CharSequence? { + val list = items + for (i in (0..position).reversed()) { + val item = list.getOrNull(i) ?: continue + if (item is DateTimeAgo) { + return item.format(context.resources) + } + } + return null + } + private class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when { @@ -44,6 +58,10 @@ class FeedAdapter( oldItem == newItem } + oldItem is LoadingFooter && newItem is LoadingFooter -> { + oldItem.key == newItem.key + } + else -> oldItem.javaClass == newItem.javaClass } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt similarity index 80% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt index ce123e0b2..c7b57d4b6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedItemAD.kt @@ -4,16 +4,16 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.isBold +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemFeedBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.isBold -import org.koitharu.kotatsu.utils.ext.newImageRequest -import org.koitharu.kotatsu.utils.ext.source fun feedItemAD( coil: ImageLoader, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/FeedItem.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/ListModelConversionExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt index 56f213dd1..047b0d7ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesActivity.kt @@ -9,7 +9,7 @@ import androidx.fragment.app.commit import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityContainerBinding import org.koitharu.kotatsu.main.ui.owners.AppBarOwner @@ -19,7 +19,7 @@ class UpdatesActivity : AppBarOwner { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,7 +36,7 @@ class UpdatesActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesFragment.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt similarity index 53% rename from app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt index bebfb7a58..a8c437c6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt @@ -3,28 +3,24 @@ 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.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.parser.MangaTagHighlighter import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.ListModel 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.list.ui.model.toUi 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 @@ -32,16 +28,16 @@ class UpdatesViewModel @Inject constructor( private val repository: TrackingRepository, private val settings: AppSettings, private val historyRepository: HistoryRepository, - private val tagHighlighter: MangaTagHighlighter, + private val extraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { override val content = combine( repository.observeUpdatedManga(), - listModeFlow, - ) { mangaMap, mode -> + listMode, + ) { mangaList, mode -> when { - mangaMap.isEmpty() -> listOf( + mangaList.isEmpty() -> listOf( EmptyState( icon = R.drawable.ic_empty_history, textPrimary = R.string.text_history_holder_primary, @@ -50,7 +46,7 @@ class UpdatesViewModel @Inject constructor( ), ) - else -> mapList(mangaMap, mode) + else -> mangaList.toUi(mode, extraProvider) } }.onStart { loadingCounter.increment() @@ -58,24 +54,9 @@ class UpdatesViewModel @Inject constructor( loadingCounter.decrement() }.catch { emit(listOf(it.toErrorState(canRetry = false))) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) override fun onRefresh() = Unit override fun onRetry() = Unit - - private suspend fun mapList( - mangaMap: Map, - mode: ListMode, - ): List { - 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, tagHighlighter) - ListMode.GRID -> manga.toGridModel(counter, percent) - } - } - } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index fc32bf3ee..934a19ac0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -11,8 +11,7 @@ import androidx.core.app.NotificationCompat.VISIBILITY_SECRET import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat import androidx.hilt.work.HiltWorker -import androidx.lifecycle.LiveData -import androidx.lifecycle.map +import androidx.lifecycle.asFlow import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.CoroutineWorker @@ -35,20 +34,22 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.logs.FileLogger import org.koitharu.kotatsu.core.logs.TrackerLogger import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull +import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.toBitmapOrNull -import org.koitharu.kotatsu.utils.ext.trySetForeground import java.util.concurrent.TimeUnit @HiltWorker @@ -84,9 +85,7 @@ class TrackWorker @AssistedInject constructor( if (!settings.isTrackerEnabled) { return Result.success(workDataOf(0, 0)) } - if (TAG in tags) { // not expedited - trySetForeground() - } + trySetForeground() val tracks = tracker.getAllTracks() logger.log("Total ${tracks.size} tracks") if (tracks.isEmpty()) { @@ -265,11 +264,13 @@ class TrackWorker @AssistedInject constructor( WorkManager.getInstance(context).enqueue(request) } - fun getIsRunningLiveData(context: Context): LiveData { + fun observeIsRunning(context: Context): Flow { val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build() - return WorkManager.getInstance(context).getWorkInfosLiveData(query).map { works -> - works.any { x -> x.state == WorkInfo.State.RUNNING } - } + return WorkManager.getInstance(context).getWorkInfosLiveData(query) + .asFlow() + .map { works -> + works.any { x -> x.state == WorkInfo.State.RUNNING } + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackingItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackingItem.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/WidgetUpdater.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/WidgetUpdater.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt index f88a0525a..83266be1e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt @@ -12,11 +12,11 @@ import coil.size.Size import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.core.parser.MangaIntent +import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow +import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.replaceWith -import org.koitharu.kotatsu.utils.ext.getDrawableOrThrow class RecentListFactory( private val context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt index 250892e28..a5052477a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt @@ -4,7 +4,7 @@ import android.content.Intent import android.widget.RemoteViewsService import coil.ImageLoader import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.data.HistoryRepository import javax.inject.Inject @AndroidEntryPoint diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt index b5d3f5eb0..e51c3c912 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt @@ -13,10 +13,12 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter import org.koitharu.kotatsu.widget.shelf.model.CategoryItem @@ -41,10 +43,10 @@ class ShelfConfigActivity : setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } adapter = CategorySelectAdapter(this) - binding.recyclerView.adapter = adapter - binding.buttonDone.isVisible = true - binding.buttonDone.setOnClickListener(this) - binding.fabAdd.hide() + viewBinding.recyclerView.adapter = adapter + viewBinding.buttonDone.isVisible = true + viewBinding.buttonDone.setOnClickListener(this) + viewBinding.fabAdd.hide() val appWidgetId = intent?.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID, @@ -57,7 +59,7 @@ class ShelfConfigActivity : viewModel.checkedId = config.categoryId viewModel.content.observe(this, this::onContentChanged) - viewModel.onError.observe(this, SnackbarErrorObserver(binding.recyclerView, null)) + viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) } override fun onClick(v: View) { @@ -79,17 +81,17 @@ class ShelfConfigActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.fabAdd.updateLayoutParams { + viewBinding.fabAdd.updateLayoutParams { rightMargin = topMargin + insets.right leftMargin = topMargin + insets.left bottomMargin = topMargin + insets.bottom } - binding.recyclerView.updatePadding( + viewBinding.recyclerView.updatePadding( left = insets.left, right = insets.right, bottom = insets.bottom, ) - with(binding.toolbar) { + with(viewBinding.toolbar) { updatePadding( left = insets.left, right = insets.right, diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt similarity index 66% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt index d8d2f2599..3a1b4deca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfConfigViewModel.kt @@ -1,14 +1,16 @@ package org.koitharu.kotatsu.widget.shelf -import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import org.koitharu.kotatsu.base.ui.BaseViewModel +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus +import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.widget.shelf.model.CategoryItem import javax.inject.Inject @@ -19,7 +21,7 @@ class ShelfConfigViewModel @Inject constructor( private val selectedCategoryId = MutableStateFlow(0L) - val content: LiveData> = combine( + val content: StateFlow> = combine( favouritesRepository.observeCategories(), selectedCategoryId, ) { categories, selectedId -> @@ -29,7 +31,11 @@ class ShelfConfigViewModel @Inject constructor( CategoryItem(it.id, it.title, selectedId == it.id) } list - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) - var checkedId: Long by selectedCategoryId::value + var checkedId: Long + get() = selectedCategoryId.value + set(value) { + selectedCategoryId.value = value + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt index 74caff390..fb914916f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt @@ -12,12 +12,12 @@ import coil.size.Size import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.MangaIntent +import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.prefs.AppWidgetConfig +import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.replaceWith -import org.koitharu.kotatsu.utils.ext.getDrawableOrThrow class ShelfListFactory( private val context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt index 39f90c22e..10fac5806 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectAdapter.kt @@ -2,7 +2,7 @@ package org.koitharu.kotatsu.widget.shelf.adapter import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.widget.shelf.model.CategoryItem class CategorySelectAdapter( @@ -30,4 +30,4 @@ class CategorySelectAdapter( return super.getChangePayload(oldItem, newItem) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt index ce4ded99d..9de2ab3b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/adapter/CategorySelectItemAD.kt @@ -2,14 +2,14 @@ package org.koitharu.kotatsu.widget.shelf.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemCategoryCheckableSingleBinding import org.koitharu.kotatsu.widget.shelf.model.CategoryItem fun categorySelectItemAD( clickListener: OnListItemClickListener ) = adapterDelegateViewBinding( - { inflater, parent -> ItemCategoryCheckableSingleBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemCategoryCheckableSingleBinding.inflate(inflater, parent, false) }, ) { itemView.setOnClickListener { @@ -22,4 +22,4 @@ fun categorySelectItemAD( isChecked = item.isSelected } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/model/CategoryItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/model/CategoryItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/widget/shelf/model/CategoryItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/widget/shelf/model/CategoryItem.kt diff --git a/app/src/main/res/drawable/bg_badge_primary.xml b/app/src/main/res/drawable/bg_badge_primary.xml new file mode 100644 index 000000000..1393b8638 --- /dev/null +++ b/app/src/main/res/drawable/bg_badge_primary.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_data_privacy.xml b/app/src/main/res/drawable/ic_data_privacy.xml new file mode 100644 index 000000000..0697d42ee --- /dev/null +++ b/app/src/main/res/drawable/ic_data_privacy.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_error_small.xml b/app/src/main/res/drawable/ic_error_small.xml new file mode 100644 index 000000000..964da43cc --- /dev/null +++ b/app/src/main/res/drawable/ic_error_small.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_new.xml b/app/src/main/res/drawable/ic_new.xml index f51c265cf..f0e81e12d 100644 --- a/app/src/main/res/drawable/ic_new.xml +++ b/app/src/main/res/drawable/ic_new.xml @@ -2,7 +2,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" - android:tint="?colorControlNormal" + android:tint="?colorError" android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" /> diff --git a/app/src/main/res/drawable/ic_list_create.xml b/app/src/main/res/drawable/ic_sort.xml similarity index 64% rename from app/src/main/res/drawable/ic_list_create.xml rename to app/src/main/res/drawable/ic_sort.xml index dce66429c..944c99fd4 100644 --- a/app/src/main/res/drawable/ic_list_create.xml +++ b/app/src/main/res/drawable/ic_sort.xml @@ -7,6 +7,6 @@ android:viewportWidth="24" android:viewportHeight="24"> - \ No newline at end of file + android:fillColor="#000000" + android:pathData="M18 21L14 17H17V7H14L18 3L22 7H19V17H22M2 19V17H12V19M2 13V11H9V13M2 7V5H6V7H2Z" /> + diff --git a/app/src/main/res/drawable/ic_tap.xml b/app/src/main/res/drawable/ic_tap.xml new file mode 100644 index 000000000..d0bdf143c --- /dev/null +++ b/app/src/main/res/drawable/ic_tap.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout-land/item_empty_state.xml b/app/src/main/res/layout-land/item_empty_state.xml new file mode 100644 index 000000000..f039c9cbc --- /dev/null +++ b/app/src/main/res/layout-land/item_empty_state.xml @@ -0,0 +1,47 @@ + + + + + + + + + +