diff --git a/app/build.gradle b/app/build.gradle index f0dde7b78..64aab0ef4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 33 - versionCode 540 - versionName '5.0.2' + versionCode 546 + versionName '5.1.2' 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 @@ -78,15 +79,15 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:96b9ac36f3') { + implementation('com.github.KotatsuApp:kotatsu-parsers:ebcc6391d6') { exclude group: 'org.json', module: 'json' } implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-ktx:1.10.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.activity:activity-ktx:1.7.1' implementation 'androidx.fragment:fragment-ktx:1.5.7' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' @@ -98,12 +99,19 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' - implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'com.google.android.material:material:1.9.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1' + implementation 'androidx.work:work-runtime-ktx:2.8.1' + //noinspection GradleDependency + implementation('com.google.guava:guava:31.1-android') { + exclude group: 'com.google.guava', module: 'failureaccess' + exclude group: 'org.checkerframework', module: 'checker-qual' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + } + implementation 'androidx.room:room-runtime:2.5.1' implementation 'androidx.room:room-ktx:2.5.1' kapt 'androidx.room:room-compiler:2.5.1' @@ -115,8 +123,8 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' - implementation 'com.google.dagger:hilt-android:2.45' - kapt 'com.google.dagger:hilt-compiler:2.45' + implementation 'com.google.dagger:hilt-android:2.46.1' + kapt 'com.google.dagger:hilt-compiler:2.46.1' implementation 'androidx.hilt:hilt-work:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0' @@ -133,18 +141,18 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20230227' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' - androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' androidTestImplementation 'androidx.room:room-testing:2.5.1' - androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0' + androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0' - androidTestImplementation 'com.google.dagger:hilt-android-testing:2.45' - kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.45' + androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1' + kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f2f5b932b..a81d9ac31 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,4 +1,5 @@ -optimizationpasses 8 +-dontobfuscate -assumenosideeffects class kotlin.jvm.internal.Intrinsics { public static void checkExpressionValueIsNotNull(...); public static void checkNotNullExpressionValue(...); @@ -7,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 f4448baa4..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 @@ -5,8 +5,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 java.io.File -import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.* @@ -19,7 +17,9 @@ 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 @HiltAndroidTest @RunWith(AndroidJUnit4::class) @@ -52,6 +52,7 @@ class AppBackupAgentTest { title = SampleData.favouriteCategory.title, sortOrder = SampleData.favouriteCategory.order, isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled, + isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary, ) favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga)) historyRepository.addOrUpdate( 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/util/LoggingAdapterDataObserver.kt b/app/src/debug/java/org/koitharu/kotatsu/util/LoggingAdapterDataObserver.kt new file mode 100644 index 000000000..07ae3c4bb --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/util/LoggingAdapterDataObserver.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.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/util/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/util/ext/DebugExt.kt new file mode 100644 index 000000000..acaab0f0c --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/util/ext/DebugExt.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.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 fb10865a6..128fba69f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,7 @@ + @@ -174,10 +175,6 @@ - + android:exported="false" + tools:node="remove"> + + (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/core/cache/DeferredLruCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt deleted file mode 100644 index 8b9e08aa3..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.koitharu.kotatsu.core.cache - -import androidx.collection.LruCache - -class DeferredLruCache(maxSize: Int) : LruCache>(maxSize) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt deleted file mode 100644 index d0b5a23c3..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/details/domain/BranchComparator.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.details.domain - -class BranchComparator : Comparator { - - override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt deleted file mode 100644 index bffa668a0..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ /dev/null @@ -1,340 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.text.Html -import android.text.SpannableString -import android.text.Spanned -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.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterNotNull -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.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.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.details.domain.BranchComparator -import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.HistoryInfo -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.local.data.LocalStorageChanges -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.mapToSet -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, -) : BaseViewModel() { - - private var loadingJob: Job - - val onShowToast = SingleLiveEvent() - - private val history = historyRepository.observeOne(delegate.mangaId) - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - - private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() } - .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) - - private val chaptersQuery = MutableStateFlow("") - - 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 historyInfo: LiveData = combine( - delegate.manga, - delegate.selectedBranch, - history, - historyRepository.observeShouldSkip(delegate.manga), - ) { m, b, h, im -> - HistoryInfo(m, b, h, im) - }.asFlowLiveData( - context = viewModelScope.coroutineContext + Dispatchers.Default, - defaultValue = HistoryInfo(null, null, null, false), - ) - - val bookmarks = delegate.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) - - val description = delegate.manga - .distinctUntilChangedBy { it?.description.orEmpty() } - .transformLatest { - val description = it?.description - if (description.isNullOrEmpty()) { - emit(null) - } else { - emit(description.parseAsHtml().filterSpans()) - emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans()) - } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null) - - val onMangaRemoved = SingleLiveEvent() - 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 branches: LiveData> = delegate.manga.map { - val chapters = it?.chapters ?: return@map emptyList() - chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator()) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - - val selectedBranchIndex = combine( - branches.asFlow(), - delegate.selectedBranch, - ) { branches, selected -> - branches.indexOf(selected) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, -1) - - 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 chapters = combine( - combine( - delegate.manga, - delegate.relatedManga, - history, - delegate.selectedBranch, - newChapters, - ) { manga, related, history, branch, news -> - delegate.mapChapters(manga, related, history, news, branch) - }, - chaptersReversed, - chaptersQuery, - ) { list, reversed, query -> - (if (reversed) list.asReversed() else list).filterSearch(query) - }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) - - val selectedBranchValue: String? - get() = delegate.selectedBranch.value - - init { - loadingJob = doLoad() - launchJob(Dispatchers.Default) { - localStorageChanges - .collect { onDownloadComplete(it) } - } - } - - fun reload() { - loadingJob.cancel() - loadingJob = doLoad() - } - - fun deleteLocal() { - val m = delegate.manga.value - 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) - } - } - - fun removeBookmark(bookmark: Bookmark) { - launchJob { - bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId) - onShowToast.call(R.string.bookmark_removed) - } - } - - fun setChaptersReversed(newValue: Boolean) { - settings.chaptersReverse = newValue - } - - fun setSelectedBranch(branch: String?) { - delegate.selectedBranch.value = branch - } - - fun getRemoteManga(): Manga? { - return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } - } - - fun performChapterSearch(query: String?) { - chaptersQuery.value = query?.trim().orEmpty() - } - - fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) { - val scrobbler = getScrobbler(index) ?: return - launchJob(Dispatchers.Default) { - scrobbler.updateScrobblingInfo( - mangaId = delegate.mangaId, - rating = rating, - status = status, - comment = null, - ) - } - } - - fun unregisterScrobbling(index: Int) { - val scrobbler = getScrobbler(index) ?: return - launchJob(Dispatchers.Default) { - scrobbler.unregisterScrobbling( - mangaId = delegate.mangaId, - ) - } - } - - fun markChapterAsCurrent(chapterId: Long) { - launchJob(Dispatchers.Default) { - val manga = checkNotNull(delegate.manga.value) - val chapters = checkNotNull(manga.getChapters(selectedBranchValue)) - 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) - } - } - - private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - delegate.doLoad() - } - - private fun List.filterSearch(query: String): List { - if (query.isEmpty() || this.isEmpty()) { - return this - } - return filter { - it.chapter.name.contains(query, ignoreCase = true) - } - } - - 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() - } - } - } - } - - private fun Spanned.filterSpans(): CharSequence { - val spannable = SpannableString.valueOf(this) - val spans = spannable.getSpans() - for (span in spans) { - spannable.removeSpan(span) - } - return spannable.trim() - } - - private fun getScrobbler(index: Int): Scrobbler? { - val info = scrobblingInfo.value?.getOrNull(index) - val scrobbler = if (info != null) { - scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable } - } else { - null - } - if (scrobbler == null) { - errorEvent.call(IllegalStateException("Scrobbler [$index] is not available")) - } - return scrobbler - } -} 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/BranchesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt deleted file mode 100644 index 2ca602df9..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.koitharu.kotatsu.details.ui.adapter - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.TextView -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.parsers.util.replaceWith - -class BranchesAdapter : BaseAdapter() { - - private val dataSet = ArrayList() - - override fun getCount(): Int { - return dataSet.size - } - - override fun getItem(position: Int): Any? { - return dataSet[position] - } - - override fun getItemId(position: Int): Long { - return dataSet[position].hashCode().toLong() - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: LayoutInflater.from(parent.context) - .inflate(R.layout.item_branch, parent, false) - (view as TextView).text = dataSet[position] - return view - } - - override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: LayoutInflater.from(parent.context) - .inflate(R.layout.item_branch_dropdown, parent, false) - (view as TextView).text = dataSet[position] - return view - } - - fun setItems(items: Collection) { - dataSet.replaceWith(items) - notifyDataSetChanged() - } -} \ No newline at end of file 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/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt deleted file mode 100644 index 88f4a6aa0..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ /dev/null @@ -1,281 +0,0 @@ -package org.koitharu.kotatsu.download.domain - -import android.app.Service -import android.content.Context -import android.webkit.MimeTypeMap -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.lifecycleScope -import coil.ImageLoader -import coil.request.ImageRequest -import coil.size.Scale -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.android.scopes.ServiceScoped -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.internal.closeQuietly -import okio.IOException -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.network.CommonHeaders -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.download.ui.service.PausingHandle -import org.koitharu.kotatsu.local.data.LocalManga -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.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.utils.ext.copyToSuspending -import org.koitharu.kotatsu.utils.ext.deleteAwait -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.progress.PausingProgressJob -import java.io.File -import javax.inject.Inject - -private const val MAX_FAILSAFE_ATTEMPTS = 2 -private const val DOWNLOAD_ERROR_DELAY = 500L -private const val SLOWDOWN_DELAY = 150L - -@ServiceScoped -class DownloadManager @Inject constructor( - service: Service, - @ApplicationContext private val context: Context, - private val imageLoader: ImageLoader, - private val okHttp: OkHttpClient, - private val cache: PagesCache, - private val localMangaRepository: LocalMangaRepository, - private val settings: AppSettings, - private val mangaRepositoryFactory: MangaRepository.Factory, - @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, -) { - - private val coverWidth = context.resources.getDimensionPixelSize( - androidx.core.R.dimen.compat_notification_large_icon_max_width, - ) - private val coverHeight = context.resources.getDimensionPixelSize( - androidx.core.R.dimen.compat_notification_large_icon_max_height, - ) - private val semaphore = Semaphore(settings.downloadsParallelism) - private val coroutineScope = (service as LifecycleService).lifecycleScope - - fun downloadManga( - manga: Manga, - chaptersIds: LongArray?, - startId: Int, - ): PausingProgressJob { - val stateFlow = MutableStateFlow( - DownloadState.Queued(startId = startId, manga = manga, cover = null), - ) - val pausingHandle = PausingHandle() - val job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(stateFlow)) { - try { - downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId) - } catch (e: CancellationException) { // handle cancellation if not handled already - val state = stateFlow.value - if (state !is DownloadState.Cancelled) { - stateFlow.value = DownloadState.Cancelled(startId, state.manga, state.cover) - } - throw e - } - } - return PausingProgressJob(job, stateFlow, pausingHandle) - } - - private suspend fun downloadMangaImpl( - manga: Manga, - chaptersIds: LongArray?, - outState: MutableStateFlow, - pausingHandle: PausingHandle, - startId: Int, - ) { - @Suppress("NAME_SHADOWING") - var manga = manga - val chaptersIdsSet = chaptersIds?.toMutableSet() - val cover = loadCover(manga) - outState.value = DownloadState.Queued(startId, manga, cover) - withMangaLock(manga) { - semaphore.withPermit { - outState.value = DownloadState.Preparing(startId, manga, null) - val destination = localMangaRepository.getOutputDir(manga) - checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } - val tempFileName = "${manga.id}_$startId.tmp" - var output: LocalMangaOutput? = null - try { - if (manga.source == MangaSource.LOCAL) { - manga = localMangaRepository.getRemoteManga(manga) - ?: error("Cannot obtain remote manga instance") - } - val repo = mangaRepositoryFactory.create(manga.source) - outState.value = DownloadState.Preparing(startId, manga, cover) - val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga - output = LocalMangaOutput.getOrCreate(destination, data) - val coverUrl = data.largeCoverUrl.ifNullOrEmpty { data.coverUrl } - if (coverUrl.isNotEmpty()) { - downloadFile(coverUrl, destination, tempFileName, repo.source).let { file -> - output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) - } - } - val chapters = checkNotNull( - if (chaptersIdsSet == null) { - data.chapters - } else { - data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) } - }, - ) { "Chapters list must not be null" } - check(chapters.isNotEmpty()) { "Chapters list must not be empty" } - check(chaptersIdsSet.isNullOrEmpty()) { - "${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga" - } - for ((chapterIndex, chapter) in chapters.withIndex()) { - val pages = runFailsafe(outState, pausingHandle) { - repo.getPages(chapter) - } - for ((pageIndex, page) in pages.withIndex()) { - runFailsafe(outState, pausingHandle) { - val url = repo.getPageUrl(page) - val file = cache.get(url) - ?: downloadFile(url, destination, tempFileName, repo.source) - output.addPage( - chapter = chapter, - file = file, - pageNumber = pageIndex, - ext = MimeTypeMap.getFileExtensionFromUrl(url), - ) - } - outState.value = DownloadState.Progress( - startId = startId, - manga = data, - cover = cover, - totalChapters = chapters.size, - currentChapter = chapterIndex, - totalPages = pages.size, - currentPage = pageIndex, - ) - - if (settings.isDownloadsSlowdownEnabled) { - delay(SLOWDOWN_DELAY) - } - } - if (output.flushChapter(chapter)) { - runCatchingCancellable { - localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga()) - }.onFailure(Throwable::printStackTraceDebug) - } - } - outState.value = DownloadState.PostProcessing(startId, data, cover) - output.mergeWithExisting() - output.finish() - val localManga = LocalMangaInput.of(output.rootFile).getManga() - localStorageChanges.emit(localManga) - outState.value = DownloadState.Done(startId, data, cover, localManga.manga) - } catch (e: CancellationException) { - outState.value = DownloadState.Cancelled(startId, manga, cover) - throw e - } catch (e: Throwable) { - e.printStackTraceDebug() - outState.value = DownloadState.Error(startId, manga, cover, e, false) - } finally { - withContext(NonCancellable) { - output?.closeQuietly() - output?.cleanup() - File(destination, tempFileName).deleteAwait() - } - } - } - } - } - - private suspend fun runFailsafe( - outState: MutableStateFlow, - pausingHandle: PausingHandle, - block: suspend () -> R, - ): R { - var countDown = MAX_FAILSAFE_ATTEMPTS - failsafe@ while (true) { - try { - return block() - } catch (e: IOException) { - if (countDown <= 0) { - val state = outState.value - outState.value = DownloadState.Error(state.startId, state.manga, state.cover, e, true) - countDown = MAX_FAILSAFE_ATTEMPTS - pausingHandle.pause() - pausingHandle.awaitResumed() - outState.value = state - } else { - countDown-- - delay(DOWNLOAD_ERROR_DELAY) - } - } - } - } - - private suspend fun downloadFile( - url: String, - destination: File, - tempFileName: String, - source: MangaSource, - ): File { - val request = Request.Builder() - .url(url) - .tag(MangaSource::class.java, source) - .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) - .get() - .build() - val call = okHttp.newCall(request) - val file = File(destination, tempFileName) - val response = call.clone().await() - file.outputStream().use { out -> - checkNotNull(response.body).byteStream().copyToSuspending(out) - } - return file - } - - private fun errorStateHandler(outState: MutableStateFlow) = - CoroutineExceptionHandler { _, throwable -> - throwable.printStackTraceDebug() - val prevValue = outState.value - outState.value = DownloadState.Error( - startId = prevValue.startId, - manga = prevValue.manga, - cover = prevValue.cover, - error = throwable, - canRetry = false, - ) - } - - private suspend fun loadCover(manga: Manga) = runCatchingCancellable { - imageLoader.execute( - ImageRequest.Builder(context) - .data(manga.coverUrl) - .allowHardware(false) - .tag(manga.source) - .size(coverWidth, coverHeight) - .scale(Scale.FILL) - .build(), - ).drawable - }.getOrNull() - - private suspend inline fun withMangaLock(manga: Manga, block: () -> T) = try { - localMangaRepository.lockManga(manga.id) - block() - } finally { - localMangaRepository.unlockManga(manga.id) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt deleted file mode 100644 index 0b874f6df..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt +++ /dev/null @@ -1,234 +0,0 @@ -package org.koitharu.kotatsu.download.domain - -import android.graphics.drawable.Drawable -import org.koitharu.kotatsu.parsers.model.Manga - -sealed interface DownloadState { - - val startId: Int - val manga: Manga - val cover: Drawable? - - override fun equals(other: Any?): Boolean - - override fun hashCode(): Int - - val isTerminal: Boolean - get() = this is Done || this is Cancelled || (this is Error && !canRetry) - - class Queued( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Queued - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - return result - } - } - - class Preparing( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Preparing - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - return result - } - } - - class Progress( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - val totalChapters: Int, - val currentChapter: Int, - val totalPages: Int, - val currentPage: Int, - ) : DownloadState { - - val max: Int = totalChapters * totalPages - - val progress: Int = totalPages * currentChapter + currentPage + 1 - - val percent: Float = progress.toFloat() / max - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Progress - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - if (totalChapters != other.totalChapters) return false - if (currentChapter != other.currentChapter) return false - if (totalPages != other.totalPages) return false - if (currentPage != other.currentPage) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - result = 31 * result + totalChapters - result = 31 * result + currentChapter - result = 31 * result + totalPages - result = 31 * result + currentPage - return result - } - } - - class Done( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - val localManga: Manga, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Done - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - if (localManga != other.localManga) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - result = 31 * result + localManga.hashCode() - return result - } - } - - class Error( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - val error: Throwable, - val canRetry: Boolean, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Error - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - if (error != other.error) return false - if (canRetry != other.canRetry) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - result = 31 * result + error.hashCode() - result = 31 * result + canRetry.hashCode() - return result - } - } - - class Cancelled( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Cancelled - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - return result - } - } - - class PostProcessing( - override val startId: Int, - override val manga: Manga, - override val cover: Drawable?, - ) : DownloadState { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PostProcessing - - if (startId != other.startId) return false - if (manga != other.manga) return false - if (cover != other.cover) return false - - return true - } - - override fun hashCode(): Int { - var result = startId - result = 31 * result + manga.hashCode() - result = 31 * result + (cover?.hashCode() ?: 0) - return result - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt deleted file mode 100644 index 2afba0b27..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt +++ /dev/null @@ -1,140 +0,0 @@ -package org.koitharu.kotatsu.download.ui - -import android.view.View -import androidx.core.view.isVisible -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import coil.ImageLoader -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.databinding.ItemDownloadBinding -import org.koitharu.kotatsu.details.ui.DetailsActivity -import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.parsers.util.format -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.onFirst -import org.koitharu.kotatsu.utils.ext.source - -fun downloadItemAD( - lifecycleOwner: LifecycleOwner, - coil: ImageLoader, -) = adapterDelegateViewBinding( - { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }, -) { - var job: Job? = null - val percentPattern = context.resources.getString(R.string.percent_string_pattern) - - val clickListener = View.OnClickListener { v -> - when (v.id) { - R.id.button_cancel -> item.cancel() - R.id.button_resume -> item.resume() - else -> context.startActivity( - DetailsActivity.newIntent(context, item.progressValue.manga), - ) - } - } - binding.buttonCancel.setOnClickListener(clickListener) - binding.buttonResume.setOnClickListener(clickListener) - itemView.setOnClickListener(clickListener) - - bind { - job?.cancel() - job = item.progressAsFlow().onFirst { state -> - binding.imageViewCover.newImageRequest(lifecycleOwner, state.manga.coverUrl)?.run { - placeholder(state.cover) - fallback(R.drawable.ic_placeholder) - error(R.drawable.ic_error_placeholder) - source(state.manga.source) - allowRgb565(true) - enqueueWith(coil) - } - }.onEach { state -> - binding.textViewTitle.text = state.manga.title - when (state) { - is DownloadState.Cancelled -> { - binding.textViewStatus.setText(R.string.cancelling_) - binding.progressBar.isIndeterminate = true - binding.progressBar.isVisible = true - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = false - binding.buttonResume.isVisible = false - } - - is DownloadState.Done -> { - binding.textViewStatus.setText(R.string.download_complete) - binding.progressBar.isIndeterminate = false - binding.progressBar.isVisible = false - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = false - binding.buttonResume.isVisible = false - } - - is DownloadState.Error -> { - binding.textViewStatus.setText(R.string.error_occurred) - binding.progressBar.isIndeterminate = false - binding.progressBar.isVisible = false - binding.textViewPercent.isVisible = false - binding.textViewDetails.text = state.error.getDisplayMessage(context.resources) - binding.textViewDetails.isVisible = true - binding.buttonCancel.isVisible = state.canRetry - binding.buttonResume.isVisible = state.canRetry - } - - is DownloadState.PostProcessing -> { - binding.textViewStatus.setText(R.string.processing_) - binding.progressBar.isIndeterminate = true - binding.progressBar.isVisible = true - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = false - binding.buttonResume.isVisible = false - } - - is DownloadState.Preparing -> { - binding.textViewStatus.setText(R.string.preparing_) - binding.progressBar.isIndeterminate = true - binding.progressBar.isVisible = true - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = true - binding.buttonResume.isVisible = false - } - - is DownloadState.Progress -> { - binding.textViewStatus.setText(R.string.manga_downloading_) - binding.progressBar.isIndeterminate = false - binding.progressBar.isVisible = true - binding.progressBar.max = state.max - binding.progressBar.setProgressCompat(state.progress, true) - binding.textViewPercent.text = percentPattern.format((state.percent * 100f).format(1)) - binding.textViewPercent.isVisible = true - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = true - binding.buttonResume.isVisible = false - } - - is DownloadState.Queued -> { - binding.textViewStatus.setText(R.string.queued) - binding.progressBar.isIndeterminate = false - binding.progressBar.isVisible = false - binding.textViewPercent.isVisible = false - binding.textViewDetails.isVisible = false - binding.buttonCancel.isVisible = true - binding.buttonResume.isVisible = false - } - } - }.launchIn(lifecycleOwner.lifecycleScope) - } - - onViewRecycled { - job?.cancel() - job = null - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt deleted file mode 100644 index 7b0872910..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.koitharu.kotatsu.download.ui - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.graphics.Insets -import androidx.core.view.isVisible -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.decor.SpacingItemDecoration -import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding -import javax.inject.Inject - -@AndroidEntryPoint -class DownloadsActivity : BaseActivity() { - - @Inject - lateinit var coil: ImageLoader - - private lateinit var serviceConnection: DownloadsConnection - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityDownloadsBinding.inflate(layoutInflater)) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - val adapter = DownloadsAdapter(this, coil) - val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) - binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.adapter = adapter - serviceConnection = DownloadsConnection(this, this) - serviceConnection.items.observe(this) { items -> - adapter.items = items - binding.textViewHolder.isVisible = items.isNullOrEmpty() - } - serviceConnection.bind() - } - - override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( - left = insets.left, - right = insets.right, - bottom = insets.bottom, - ) - binding.toolbar.updatePadding( - left = insets.left, - right = insets.right, - ) - } - - companion object { - - fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt deleted file mode 100644 index 5962220c3..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.koitharu.kotatsu.download.ui - -import androidx.lifecycle.LifecycleOwner -import androidx.recyclerview.widget.DiffUtil -import coil.ImageLoader -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.utils.progress.PausingProgressJob - -typealias DownloadItem = PausingProgressJob - -class DownloadsAdapter( - lifecycleOwner: LifecycleOwner, - coil: ImageLoader, -) : AsyncListDifferDelegationAdapter(DiffCallback()) { - - init { - delegatesManager.addDelegate(downloadItemAD(lifecycleOwner, coil)) - setHasStableIds(true) - } - - override fun getItemId(position: Int): Long { - return items[position].progressValue.startId.toLong() - } - - private class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame( - oldItem: DownloadItem, - newItem: DownloadItem, - ): Boolean { - return oldItem.progressValue.startId == newItem.progressValue.startId - } - - override fun areContentsTheSame( - oldItem: DownloadItem, - newItem: DownloadItem, - ): Boolean { - return oldItem.progressValue == newItem.progressValue && oldItem.isPaused == newItem.isPaused - } - - override fun getChangePayload(oldItem: DownloadItem, newItem: DownloadItem): Any { - return Unit - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsConnection.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsConnection.kt deleted file mode 100644 index f2577ec26..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsConnection.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.koitharu.kotatsu.download.ui - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.download.ui.service.DownloadService -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.progress.PausingProgressJob - -class DownloadsConnection( - private val context: Context, - private val lifecycleOwner: LifecycleOwner, -) : ServiceConnection { - - private var bindingObserver: BindingLifecycleObserver? = null - private var collectJob: Job? = null - private val itemsFlow = MutableStateFlow>>(emptyList()) - - val items - get() = itemsFlow.asFlowLiveData() - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - collectJob?.cancel() - val binder = (service as? DownloadService.DownloadBinder) - collectJob = if (binder == null) { - null - } else { - lifecycleOwner.lifecycleScope.launch { - binder.downloads.collect { - itemsFlow.value = it - } - } - } - } - - override fun onServiceDisconnected(name: ComponentName?) { - collectJob?.cancel() - collectJob = null - itemsFlow.value = itemsFlow.value.filter { it.progressValue.isTerminal } - } - - fun bind() { - if (bindingObserver != null) { - return - } - bindingObserver = BindingLifecycleObserver().also { - lifecycleOwner.lifecycle.addObserver(it) - } - context.bindService(Intent(context, DownloadService::class.java), this, 0) - } - - fun unbind() { - bindingObserver?.let { - lifecycleOwner.lifecycle.removeObserver(it) - } - bindingObserver = null - context.unbindService(this) - } - - private inner class BindingLifecycleObserver : DefaultLifecycleObserver { - - override fun onDestroy(owner: LifecycleOwner) { - super.onDestroy(owner) - unbind() - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt deleted file mode 100644 index 103f3621d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ /dev/null @@ -1,356 +0,0 @@ -package org.koitharu.kotatsu.download.ui.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.os.Build -import android.text.format.DateUtils -import android.util.SparseArray -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.PendingIntentCompat -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import androidx.core.text.HtmlCompat -import androidx.core.text.htmlEncode -import androidx.core.text.parseAsHtml -import androidx.core.util.forEach -import androidx.core.util.isNotEmpty -import androidx.core.util.size -import com.google.android.material.R as materialR -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.details.ui.DetailsActivity -import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.download.ui.DownloadsActivity -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.ellipsize -import org.koitharu.kotatsu.parsers.util.format -import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.utils.ext.getDisplayMessage - -class DownloadNotification(private val context: Context) { - - private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val states = SparseArray() - private val groupBuilder = NotificationCompat.Builder(context, CHANNEL_ID) - - private val queueIntent = PendingIntentCompat.getActivity( - context, - REQUEST_QUEUE, - DownloadsActivity.newIntent(context), - 0, - false, - ) - - private val localListIntent = PendingIntentCompat.getActivity( - context, - REQUEST_LIST_LOCAL, - MangaListActivity.newIntent(context, MangaSource.LOCAL), - 0, - false, - ) - - init { - groupBuilder.setOnlyAlertOnce(true) - groupBuilder.setDefaults(0) - groupBuilder.color = ContextCompat.getColor(context, R.color.blue_primary) - groupBuilder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE - groupBuilder.setSilent(true) - groupBuilder.setGroup(GROUP_ID) - groupBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) - groupBuilder.setGroupSummary(true) - groupBuilder.setContentTitle(context.getString(R.string.downloading_manga)) - } - - fun buildGroupNotification(): Notification { - val style = NotificationCompat.InboxStyle(groupBuilder) - var progress = 0f - var isAllDone = true - var isInProgress = false - groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - states.forEach { _, state -> - if (state.manga.isNsfw) { - groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE) - } - val summary = when (state) { - is DownloadState.Cancelled -> { - progress++ - context.getString(R.string.cancelling_) - } - - is DownloadState.Done -> { - progress++ - context.getString(R.string.download_complete) - } - - is DownloadState.Error -> { - isAllDone = false - context.getString(R.string.error) - } - - is DownloadState.PostProcessing -> { - progress++ - isInProgress = true - isAllDone = false - context.getString(R.string.processing_) - } - - is DownloadState.Preparing -> { - isAllDone = false - isInProgress = true - context.getString(R.string.preparing_) - } - - is DownloadState.Progress -> { - isAllDone = false - isInProgress = true - progress += state.percent - context.getString(R.string.percent_string_pattern, (state.percent * 100).format()) - } - - is DownloadState.Queued -> { - isAllDone = false - isInProgress = true - context.getString(R.string.queued) - } - } - style.addLine( - context.getString( - R.string.download_summary_pattern, - state.manga.title.ellipsize(16).htmlEncode(), - summary.htmlEncode(), - ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY), - ) - } - progress = if (isInProgress) { - progress / states.size.toFloat() - } else { - 1f - } - style.setBigContentTitle( - context.getString(if (isAllDone) R.string.download_complete else R.string.downloading_manga), - ) - groupBuilder.setContentText(context.resources.getQuantityString(R.plurals.items, states.size, states.size())) - groupBuilder.setNumber(states.size) - groupBuilder.setSmallIcon( - if (isInProgress) android.R.drawable.stat_sys_download else android.R.drawable.stat_sys_download_done, - ) - groupBuilder.setContentIntent(if (isAllDone) localListIntent else queueIntent) - groupBuilder.setAutoCancel(isAllDone) - when (progress) { - 1f -> groupBuilder.setProgress(0, 0, false) - 0f -> groupBuilder.setProgress(1, 0, true) - else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), false) - } - return groupBuilder.build() - } - - fun detach() { - if (states.isNotEmpty()) { - val notification = buildGroupNotification() - manager.notify(ID_GROUP_DETACHED, notification) - } - manager.cancel(ID_GROUP) - } - - fun newItem(startId: Int) = Item(startId) - - inner class Item( - private val startId: Int, - ) { - - private val builder = NotificationCompat.Builder(context, CHANNEL_ID) - private val cancelAction = NotificationCompat.Action( - materialR.drawable.material_ic_clear_black_24dp, - context.getString(android.R.string.cancel), - PendingIntentCompat.getBroadcast( - context, - startId * 2, - DownloadService.getCancelIntent(startId), - PendingIntent.FLAG_CANCEL_CURRENT, - false, - ), - ) - private val retryAction = NotificationCompat.Action( - R.drawable.ic_restart_black, - context.getString(R.string.try_again), - PendingIntentCompat.getBroadcast( - context, - startId * 2 + 1, - DownloadService.getResumeIntent(startId), - PendingIntent.FLAG_CANCEL_CURRENT, - false, - ), - ) - - init { - builder.setOnlyAlertOnce(true) - builder.setDefaults(0) - builder.color = ContextCompat.getColor(context, R.color.blue_primary) - builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE - builder.setSilent(true) - builder.setGroup(GROUP_ID) - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) - } - - fun notify(state: DownloadState, timeLeft: Long) { - builder.setContentTitle(state.manga.title) - builder.setContentText(context.getString(R.string.manga_downloading_)) - builder.setProgress(1, 0, true) - builder.setSmallIcon(android.R.drawable.stat_sys_download) - builder.setContentIntent(queueIntent) - builder.setStyle(null) - builder.setLargeIcon(state.cover?.toBitmap()) - builder.clearActions() - builder.setSubText(null) - builder.setShowWhen(false) - builder.setVisibility( - if (state.manga.isNsfw) { - NotificationCompat.VISIBILITY_PRIVATE - } else { - NotificationCompat.VISIBILITY_PUBLIC - }, - ) - when (state) { - is DownloadState.Cancelled -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.cancelling_)) - builder.setContentIntent(null) - builder.setStyle(null) - builder.setOngoing(true) - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - - is DownloadState.Done -> { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.download_complete)) - builder.setContentIntent(createMangaIntent(context, state.localManga)) - builder.setAutoCancel(true) - builder.setSmallIcon(android.R.drawable.stat_sys_download_done) - builder.setCategory(null) - builder.setStyle(null) - builder.setOngoing(false) - builder.setShowWhen(true) - builder.setWhen(System.currentTimeMillis()) - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - - is DownloadState.Error -> { - val message = state.error.getDisplayMessage(context.resources) - builder.setProgress(0, 0, false) - builder.setSmallIcon(android.R.drawable.stat_notify_error) - builder.setSubText(context.getString(R.string.error)) - builder.setContentText(message) - builder.setAutoCancel(!state.canRetry) - builder.setOngoing(state.canRetry) - builder.setCategory(NotificationCompat.CATEGORY_ERROR) - builder.setShowWhen(true) - builder.setWhen(System.currentTimeMillis()) - builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) - if (state.canRetry) { - builder.addAction(cancelAction) - builder.addAction(retryAction) - } - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - - is DownloadState.PostProcessing -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.processing_)) - builder.setStyle(null) - builder.setOngoing(true) - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - - is DownloadState.Queued -> { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.queued)) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - builder.priority = NotificationCompat.PRIORITY_LOW - } - - is DownloadState.Preparing -> { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.preparing_)) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - - is DownloadState.Progress -> { - builder.setProgress(state.max, state.progress, false) - val percent = context.getString(R.string.percent_string_pattern, (state.percent * 100).format()) - if (timeLeft > 0L) { - val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS) - builder.setContentText(eta) - builder.setSubText(percent) - } else { - builder.setContentText(percent) - } - builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) - builder.setStyle(null) - builder.setOngoing(true) - builder.addAction(cancelAction) - builder.priority = NotificationCompat.PRIORITY_DEFAULT - } - } - val notification = builder.build() - states.append(startId, state) - updateGroupNotification() - manager.notify(TAG, startId, notification) - } - - fun dismiss() { - manager.cancel(TAG, startId) - states.remove(startId) - updateGroupNotification() - } - } - - private fun updateGroupNotification() { - val notification = buildGroupNotification() - manager.notify(ID_GROUP, notification) - } - - private fun createMangaIntent(context: Context, manga: Manga) = PendingIntentCompat.getActivity( - context, - manga.hashCode(), - DetailsActivity.newIntent(context, manga), - PendingIntent.FLAG_CANCEL_CURRENT, - false, - ) - - companion object { - - private const val TAG = "download" - private const val CHANNEL_ID = "download" - private const val GROUP_ID = "downloads" - private const val REQUEST_QUEUE = 6 - private const val REQUEST_LIST_LOCAL = 7 - const val ID_GROUP = 9999 - private const val ID_GROUP_DETACHED = 9998 - - fun createChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val manager = NotificationManagerCompat.from(context) - if (manager.getNotificationChannel(CHANNEL_ID) == null) { - val channel = NotificationChannel( - CHANNEL_ID, - context.getString(R.string.downloads), - NotificationManager.IMPORTANCE_LOW, - ) - channel.enableVibration(false) - channel.enableLights(false) - channel.setSound(null, null) - manager.createNotificationChannel(channel) - } - } - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt deleted file mode 100644 index 8dd780d45..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ /dev/null @@ -1,262 +0,0 @@ -package org.koitharu.kotatsu.download.ui.service - -import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Binder -import android.os.IBinder -import android.os.PowerManager -import android.view.View -import androidx.annotation.MainThread -import androidx.core.app.ServiceCompat -import androidx.core.content.ContextCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.transformWhile -import kotlinx.coroutines.launch -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseService -import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.download.domain.DownloadManager -import org.koitharu.kotatsu.download.domain.DownloadState -import org.koitharu.kotatsu.download.ui.DownloadsActivity -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat -import org.koitharu.kotatsu.utils.ext.throttle -import org.koitharu.kotatsu.utils.progress.PausingProgressJob -import org.koitharu.kotatsu.utils.progress.ProgressJob -import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.collections.set - -@AndroidEntryPoint -class DownloadService : BaseService() { - - private lateinit var downloadNotification: DownloadNotification - private lateinit var wakeLock: PowerManager.WakeLock - - @Inject - lateinit var downloadManager: DownloadManager - - private val jobs = LinkedHashMap>() - private val jobCount = MutableStateFlow(0) - private val controlReceiver = ControlReceiver() - - override fun onCreate() { - super.onCreate() - downloadNotification = DownloadNotification(this) - wakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager) - .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") - wakeLock.acquire(TimeUnit.HOURS.toMillis(8)) - DownloadNotification.createChannel(this) - startForeground(DownloadNotification.ID_GROUP, downloadNotification.buildGroupNotification()) - val intentFilter = IntentFilter() - intentFilter.addAction(ACTION_DOWNLOAD_CANCEL) - intentFilter.addAction(ACTION_DOWNLOAD_RESUME) - ContextCompat.registerReceiver(this, controlReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - val manga = intent?.getParcelableExtraCompat(EXTRA_MANGA)?.manga - val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS) - return if (manga != null) { - jobs[startId] = downloadManga(startId, manga, chapters) - jobCount.value = jobs.size - START_REDELIVER_INTENT - } else { - stopSelfIfIdle() - START_NOT_STICKY - } - } - - override fun onBind(intent: Intent): IBinder { - super.onBind(intent) - return DownloadBinder(this) - } - - override fun onDestroy() { - unregisterReceiver(controlReceiver) - if (wakeLock.isHeld) { - wakeLock.release() - } - super.onDestroy() - } - - private fun downloadManga( - startId: Int, - manga: Manga, - chaptersIds: LongArray?, - ): PausingProgressJob { - val job = downloadManager.downloadManga(manga, chaptersIds, startId) - listenJob(job) - return job - } - - private fun listenJob(job: ProgressJob) { - lifecycleScope.launch { - val startId = job.progressValue.startId - val notificationItem = downloadNotification.newItem(startId) - try { - val timeLeftEstimator = TimeLeftEstimator() - notificationItem.notify(job.progressValue, -1L) - job.progressAsFlow() - .onEach { state -> - if (state is DownloadState.Progress) { - timeLeftEstimator.tick(value = state.progress, total = state.max) - } else { - timeLeftEstimator.emptyTick() - } - } - .throttle { state -> if (state is DownloadState.Progress) 400L else 0L } - .whileActive() - .collect { state -> - val timeLeft = timeLeftEstimator.getEstimatedTimeLeft() - notificationItem.notify(state, timeLeft) - } - job.join() - } finally { - (job.progressValue as? DownloadState.Done)?.let { - sendBroadcast( - Intent(ACTION_DOWNLOAD_COMPLETE) - .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)), - ) - } - if (job.isCancelled) { - notificationItem.dismiss() - if (jobs.remove(startId) != null) { - jobCount.value = jobs.size - } - } else { - notificationItem.notify(job.progressValue, -1L) - } - } - }.invokeOnCompletion { - stopSelfIfIdle() - } - } - - private fun Flow.whileActive(): Flow = transformWhile { state -> - emit(state) - !state.isTerminal - } - - @MainThread - private fun stopSelfIfIdle() { - if (jobs.any { (_, job) -> job.isActive }) { - return - } - downloadNotification.detach() - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - } - - inner class ControlReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent?) { - when (intent?.action) { - ACTION_DOWNLOAD_CANCEL -> { - val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) - jobs[cancelId]?.cancel() - } - - ACTION_DOWNLOAD_RESUME -> { - val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) - jobs[cancelId]?.resume() - } - } - } - } - - class DownloadBinder(service: DownloadService) : Binder(), DefaultLifecycleObserver { - - private var downloadsStateFlow = MutableStateFlow>>(emptyList()) - - init { - service.lifecycle.addObserver(this) - service.jobCount.onEach { - downloadsStateFlow.value = service.jobs.values.toList() - }.launchIn(service.lifecycleScope) - } - - override fun onDestroy(owner: LifecycleOwner) { - owner.lifecycle.removeObserver(this) - downloadsStateFlow.value = emptyList() - super.onDestroy(owner) - } - - val downloads - get() = downloadsStateFlow.asStateFlow() - } - - companion object { - - private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL" - private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME" - - const val EXTRA_MANGA = "manga" - private const val EXTRA_CHAPTERS_IDS = "chapters_ids" - private const val EXTRA_CANCEL_ID = "cancel_id" - - fun start(view: View, manga: Manga, chaptersIds: Collection? = null) { - if (chaptersIds?.isEmpty() == true) { - return - } - val intent = Intent(view.context, DownloadService::class.java) - intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) - if (chaptersIds != null) { - intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) - } - ContextCompat.startForegroundService(view.context, intent) - showStartedSnackbar(view) - } - - fun start(view: View, manga: Collection) { - if (manga.isEmpty()) { - return - } - for (item in manga) { - val intent = Intent(view.context, DownloadService::class.java) - intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false)) - ContextCompat.startForegroundService(view.context, intent) - } - showStartedSnackbar(view) - } - - fun confirmAndStart(view: View, items: Set) { - MaterialAlertDialogBuilder(view.context) - .setTitle(R.string.save_manga) - .setMessage(R.string.batch_manga_save_confirm) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.save) { _, _ -> - start(view, items) - }.show() - } - - fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL) - .putExtra(EXTRA_CANCEL_ID, startId) - - fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME) - .putExtra(EXTRA_CANCEL_ID, startId) - - private fun showStartedSnackbar(view: View) { - Snackbar.make(view, R.string.download_started, Snackbar.LENGTH_LONG) - .setAction(R.string.details) { - it.context.startActivity(DownloadsActivity.newIntent(it.context)) - }.show() - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt deleted file mode 100644 index f8bc28e98..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.koitharu.kotatsu.explore.domain - -import javax.inject.Inject -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.SortOrder - -class ExploreRepository @Inject constructor( - private val settings: AppSettings, - private val historyRepository: HistoryRepository, - private val mangaRepositoryFactory: MangaRepository.Factory, -) { - - suspend fun findRandomManga(tagsLimit: Int): Manga { - val blacklistTagRegex = settings.getSuggestionsTagsBlacklistRegex() - val allTags = historyRepository.getPopularTags(tagsLimit).filterNot { - blacklistTagRegex?.containsMatchIn(it.title) ?: false - } - val tag = allTags.randomOrNull() - val source = checkNotNull(tag?.source ?: settings.getMangaSources(includeHidden = false).randomOrNull()) { - "No sources found" - } - val repo = mangaRepositoryFactory.create(source) - val list = repo.getList( - offset = 0, - sortOrder = if (SortOrder.UPDATED in repo.sortOrders) SortOrder.UPDATED else null, - tags = setOfNotNull(tag), - ).shuffled() - for (item in list) { - if (settings.isSuggestionsExcludeNsfw && item.isNsfw) { - continue - } - if (blacklistTagRegex != null && item.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) }) { - continue - } - return item - } - return list.random() - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt deleted file mode 100644 index 03ec95536..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -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.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.list.ui.model.ListModel -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, -) : 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, - key = AppSettings.KEY_GRID_SIZE, - valueProducer = { gridSize / 100f }, - ) - - open fun onUpdateFilter(tags: Set) = Unit - - abstract fun onRefresh() - - abstract fun onRetry() -} 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/data/output/LocalMangaOutput.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt deleted file mode 100644 index e3445f387..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.koitharu.kotatsu.local.data.output - -import okio.Closeable -import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter -import org.koitharu.kotatsu.parsers.util.toFileNameSafe -import java.io.File - -sealed class LocalMangaOutput( - val rootFile: File, -) : Closeable { - - abstract suspend fun mergeWithExisting() - - abstract suspend fun addCover(file: File, ext: String) - - abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) - - abstract suspend fun flushChapter(chapter: MangaChapter): Boolean - - abstract suspend fun finish() - - abstract suspend fun cleanup() - - companion object { - - const val ENTRY_NAME_INDEX = "index.json" - const val SUFFIX_TMP = ".tmp" - - fun getOrCreate(root: File, manga: Manga): LocalMangaOutput { - return checkNotNull(getImpl(root, manga, onlyIfExists = false)) - } - - fun get(root: File, manga: Manga): LocalMangaOutput? { - return getImpl(root, manga, onlyIfExists = true) - } - - private fun getImpl(root: File, manga: Manga, onlyIfExists: Boolean): LocalMangaOutput? { - val fileName = manga.title.toFileNameSafe() - val dir = File(root, fileName) - val zip = File(root, "$fileName.cbz") - return when { - dir.isDirectory -> LocalMangaDirOutput(dir, manga) - zip.isFile -> LocalMangaZipOutput(zip, manga) - !onlyIfExists -> LocalMangaDirOutput(dir, manga) - else -> null - } - } - } -} 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/shelf/domain/ShelfSection.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt deleted file mode 100644 index de24a9583..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.shelf.domain - -enum class ShelfSection { - - HISTORY, LOCAL, UPDATED, FAVORITES; -} diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt deleted file mode 100644 index 813208aa4..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ /dev/null @@ -1,191 +0,0 @@ -package org.koitharu.kotatsu.suggestions.ui - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.os.Build -import androidx.annotation.FloatRange -import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import androidx.hilt.work.HiltWorker -import androidx.work.* -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion -import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository -import org.koitharu.kotatsu.utils.ext.asArrayList -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable -import org.koitharu.kotatsu.utils.ext.trySetForeground -import java.util.concurrent.TimeUnit -import kotlin.math.pow - -@HiltWorker -class SuggestionsWorker @AssistedInject constructor( - @Assisted appContext: Context, - @Assisted params: WorkerParameters, - private val suggestionRepository: SuggestionRepository, - private val historyRepository: HistoryRepository, - private val appSettings: AppSettings, - private val mangaRepositoryFactory: MangaRepository.Factory, -) : CoroutineWorker(appContext, params) { - - override suspend fun doWork(): Result { - val count = doWorkImpl() - val outputData = workDataOf(DATA_COUNT to count) - return Result.success(outputData) - } - - override suspend fun getForegroundInfo(): ForegroundInfo { - val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val title = applicationContext.getString(R.string.suggestions_updating) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - WORKER_CHANNEL_ID, - title, - NotificationManager.IMPORTANCE_LOW, - ) - channel.setShowBadge(false) - channel.enableVibration(false) - channel.setSound(null, null) - channel.enableLights(false) - manager.createNotificationChannel(channel) - } - - val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) - .setContentTitle(title) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setDefaults(0) - .setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)) - .setSilent(true) - .setProgress(0, 0, true) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) - .setOngoing(true) - .build() - - return ForegroundInfo(WORKER_NOTIFICATION_ID, notification) - } - - private suspend fun doWorkImpl(): Int { - if (!appSettings.isSuggestionsEnabled) { - suggestionRepository.clear() - return 0 - } - val blacklistTagRegex = appSettings.getSuggestionsTagsBlacklistRegex() - val allTags = historyRepository.getPopularTags(TAGS_LIMIT).filterNot { - blacklistTagRegex?.containsMatchIn(it.title) ?: false - } - if (allTags.isEmpty()) { - return 0 - } - if (TAG in tags) { // not expedited - trySetForeground() - } - val tagsBySources = allTags.groupBy { x -> x.source } - val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) - val rawResults = coroutineScope { - tagsBySources.flatMap { (source, tags) -> - val repo = mangaRepositoryFactory.tryCreate(source) ?: return@flatMap emptyList() - tags.map { tag -> - async(dispatcher) { - repo.getListSafe(tag) - } - } - }.awaitAll().flatten().asArrayList() - } - if (appSettings.isSuggestionsExcludeNsfw) { - rawResults.removeAll { it.isNsfw } - } - if (blacklistTagRegex != null) { - rawResults.removeAll { - it.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) } - } - } - if (rawResults.isEmpty()) { - return 0 - } - val suggestions = rawResults.distinctBy { manga -> - manga.id - }.map { manga -> - MangaSuggestion( - manga = manga, - relevance = computeRelevance(manga.tags, allTags), - ) - }.sortedBy { it.relevance }.take(LIMIT) - suggestionRepository.replace(suggestions) - return suggestions.size - } - - @FloatRange(from = 0.0, to = 1.0) - private fun computeRelevance(mangaTags: Set, allTags: List): Float { - val maxWeight = (allTags.size + allTags.size + 1 - mangaTags.size) * mangaTags.size / 2.0 - val weight = mangaTags.sumOf { tag -> - val index = allTags.indexOf(tag) - if (index < 0) 0 else allTags.size - index - } - return (weight / maxWeight).pow(2.0).toFloat() - } - - private suspend fun MangaRepository.getListSafe(tag: MangaTag) = runCatchingCancellable { - getList(offset = 0, sortOrder = SortOrder.UPDATED, tags = setOf(tag)) - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrDefault(emptyList()) - - private fun MangaRepository.Factory.tryCreate(source: MangaSource) = runCatching { - create(source) - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrNull() - - companion object { - - private const val TAG = "suggestions" - private const val TAG_ONESHOT = "suggestions_oneshot" - private const val LIMIT = 140 - private const val TAGS_LIMIT = 20 - private const val MAX_PARALLELISM = 4 - private const val DATA_COUNT = "count" - private const val WORKER_CHANNEL_ID = "suggestion_worker" - private const val WORKER_NOTIFICATION_ID = 36 - - fun setup(context: Context) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresBatteryNotLow(true) - .build() - val request = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) - .setConstraints(constraints) - .addTag(TAG) - .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) - .build() - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) - } - - fun startNow(context: Context) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - val request = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .addTag(TAG_ONESHOT) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .build() - WorkManager.getInstance(context) - .enqueue(request) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt deleted file mode 100644 index a5343ade5..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.koitharu.kotatsu.sync.ui - -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.sync.data.SyncAuthApi -import org.koitharu.kotatsu.sync.domain.SyncAuthResult -import org.koitharu.kotatsu.utils.SingleLiveEvent -import javax.inject.Inject - -@HiltViewModel -class SyncAuthViewModel @Inject constructor( - private val api: SyncAuthApi, -) : BaseViewModel() { - - val onTokenObtained = SingleLiveEvent() - - fun obtainToken(email: String, password: String) { - launchLoadingJob(Dispatchers.Default) { - val token = api.authenticate(email, password) - val result = SyncAuthResult(email, password, token) - onTokenObtained.emitCall(result) - } - } -} 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/GZipInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/utils/GZipInterceptor.kt deleted file mode 100644 index 5da93ae8a..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/GZipInterceptor.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.koitharu.kotatsu.utils - -import okhttp3.Interceptor -import okhttp3.Response -import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING - -class GZipInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val newRequest = chain.request().newBuilder() - newRequest.addHeader(CONTENT_ENCODING, "gzip") - return chain.proceed(newRequest.build()) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt deleted file mode 100644 index edece17d7..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/PreferenceIconTarget.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.koitharu.kotatsu.utils - -import android.graphics.drawable.Drawable -import androidx.preference.Preference -import coil.target.Target - -class PreferenceIconTarget( - private val preference: Preference, -) : Target { - - override fun onError(error: Drawable?) { - preference.icon = error - } - - override fun onStart(placeholder: Drawable?) { - preference.icon = placeholder - } - - override fun onSuccess(result: Drawable) { - preference.icon = result - } -} \ No newline at end of file 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/ext/IO.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt deleted file mode 100644 index 1b05eddc3..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/IO.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.withContext -import okhttp3.ResponseBody -import org.koitharu.kotatsu.utils.progress.ProgressResponseBody -import java.io.InputStream -import java.io.OutputStream - -suspend fun InputStream.copyToSuspending( - out: OutputStream, - bufferSize: Int = DEFAULT_BUFFER_SIZE, - progressState: MutableStateFlow? = null, -): Long = withContext(Dispatchers.IO) { - val job = currentCoroutineContext()[Job] - val total = available() - var bytesCopied: Long = 0 - val buffer = ByteArray(bufferSize) - var bytes = read(buffer) - while (bytes >= 0) { - out.write(buffer, 0, bytes) - bytesCopied += bytes - job?.ensureActive() - bytes = read(buffer) - job?.ensureActive() - if (progressState != null && total > 0) { - progressState.value = bytesCopied / total.toFloat() - } - } - bytesCopied -} - -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/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt deleted file mode 100644 index 0f0ac0180..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -import android.view.View -import androidx.core.graphics.Insets - -fun Insets.end(view: View): Int { - return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) left else right -} - -fun Insets.start(view: View): Int { - return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) right else left -} \ 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/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt deleted file mode 100644 index badf5ae7c..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String { - return if (this.isNullOrEmpty()) defaultValue() else this -} - -fun String.longHashCode(): Long { - var h = 1125899906842597L - val len: Int = this.length - for (i in 0 until len) { - h = 31 * h + this[i].code - } - return h -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 57ce0686c..164095e81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -20,11 +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.ext.processLifecycleScope import javax.inject.Inject @HiltAndroidApp @@ -56,6 +57,7 @@ class KotatsuApp : Application(), Configuration.Provider { processLifecycleScope.launch(Dispatchers.Default) { setupDatabaseObservers() } + WorkServiceStopHelper(applicationContext).setup() } override fun attachBaseContext(base: Context?) { 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 100% 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 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 100% 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 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 93% 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..0c439a6bc 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.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.util.ext.printStackTraceDebug import javax.inject.Inject @Reusable 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 79% 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..f2db6cb49 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() { @@ -114,7 +116,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 +151,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 72% 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..886aba926 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, 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 87% 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..d47bbb785 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,7 +13,7 @@ class BookmarksAdapter( clickListener: OnListItemClickListener, ) : AsyncListDifferDelegationAdapter( DiffCallback(), - bookmarkListAD(coil, lifecycleOwner, clickListener) + bookmarkListAD(coil, lifecycleOwner, clickListener), ) { private class DiffCallback : DiffUtil.ItemCallback() { @@ -27,4 +27,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 76% 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 991a86d8c..f84ca4899 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -12,8 +12,9 @@ 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 com.google.android.material.R as materialR @@ -24,18 +25,20 @@ class BrowserActivity : BaseActivity(), BrowserCallback override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivityBrowserBinding.inflate(layoutInflater)) + if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) { + return + } supportActionBar?.run { 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 @@ -48,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 { @@ -70,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) { @@ -89,22 +92,22 @@ 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.destroy() } override fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading + viewBinding.progressBar.isVisible = isLoading } override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { @@ -117,10 +120,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/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/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt index 34f02003c..0a1284448 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt @@ -1,10 +1,8 @@ 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 @@ -14,13 +12,13 @@ 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.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding -import org.koitharu.kotatsu.utils.ext.withArgs import javax.inject.Inject @AndroidEntryPoint @@ -39,14 +37,13 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud url = requireArguments().getString(ARG_URL).orEmpty() } - override fun onInflateView( + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, ) = FragmentCloudflareBinding.inflate(inflater, container, false) - @SuppressLint("SetJavaScriptEnabled") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentCloudflareBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) with(binding.webView.settings) { javaScriptEnabled = true cacheMode = WebSettings.LOAD_DEFAULT @@ -64,8 +61,8 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud } override fun onDestroyView() { - binding.webView.stopLoading() - binding.webView.destroy() + requireViewBinding().webView.stopLoading() + requireViewBinding().webView.destroy() onBackPressedCallback = null super.onDestroyView() } @@ -76,18 +73,18 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud override fun onDialogCreated(dialog: AlertDialog) { super.onDialogCreated(dialog) - onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also { + onBackPressedCallback = WebViewBackPressedCallback(requireViewBinding().webView).also { dialog.onBackPressedDispatcher.addCallback(it) } } override fun onResume() { super.onResume() - binding.webView.onResume() + requireViewBinding().webView.onResume() } override fun onPause() { - binding.webView.onPause() + requireViewBinding().webView.onPause() super.onPause() } @@ -97,7 +94,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud } override fun onPageLoaded() { - bindingOrNull()?.progressBar?.isInvisible = true + viewBinding?.progressBar?.isInvisible = true } override fun onCheckPassed() { 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 68% 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 6797915b1..39b9b701b 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,50 +22,42 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -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.data.PagesCache +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 @@ -75,47 +66,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 provideOkHttpClient( - localStorageManager: LocalStorageManager, - commonHeadersInterceptor: CommonHeadersInterceptor, - mirrorSwitchInterceptor: MirrorSwitchInterceptor, - cookieJar: CookieJar, - settings: AppSettings, - ): OkHttpClient { - val cache = localStorageManager.createHttpCache() - 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) - addInterceptor(GZipInterceptor()) - addInterceptor(commonHeadersInterceptor) - addInterceptor(CloudFlareInterceptor()) - addInterceptor(mirrorSwitchInterceptor) - if (BuildConfig.DEBUG) { - addInterceptor(CurlLoggingInterceptor()) - } - }.build() - } - @Provides @Singleton fun provideNetworkState( @@ -134,14 +84,10 @@ interface AppModule { @Singleton fun provideCoil( @ApplicationContext context: Context, - okHttpClient: OkHttpClient, + @MangaHttpClient okHttpClient: OkHttpClient, mangaRepositoryFactory: MangaRepository.Factory, + pagesCache: PagesCache, ): ImageLoader { - val httpClientFactory = { - okHttpClient.newBuilder() - .cache(null) - .build() - } val diskCacheFactory = { val rootDir = context.externalCacheDir ?: context.cacheDir DiskCache.Builder() @@ -149,19 +95,20 @@ 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(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory)) .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/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt new file mode 100644 index 000000000..34d46dfca --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt @@ -0,0 +1,33 @@ +package org.koitharu.kotatsu.core.cache + +import androidx.collection.LruCache +import java.util.concurrent.TimeUnit + +class ExpiringLruCache( + val maxSize: Int, + private val lifetime: Long, + private val timeUnit: TimeUnit, +) { + + private val cache = LruCache>(maxSize) + + operator fun get(key: ContentCache.Key): T? { + val value = cache.get(key) ?: return null + if (value.isExpired) { + cache.remove(key) + } + return value.get() + } + + operator fun set(key: ContentCache.Key, value: T) { + cache.put(key, ExpiringValue(value, lifetime, timeUnit)) + } + + fun clear() { + cache.evictAll() + } + + fun trimToSize(size: Int) { + cache.trimToSize(size) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringValue.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringValue.kt new file mode 100644 index 000000000..2d561bb0c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/ExpiringValue.kt @@ -0,0 +1,34 @@ +package org.koitharu.kotatsu.core.cache + +import android.os.SystemClock +import java.util.concurrent.TimeUnit + +class ExpiringValue( + private val value: T, + lifetime: Long, + timeUnit: TimeUnit, +) { + + private val expiresAt = SystemClock.elapsedRealtime() + timeUnit.toMillis(lifetime) + + val isExpired: Boolean + get() = SystemClock.elapsedRealtime() >= expiresAt + + fun get(): T? = if (isExpired) null else value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExpiringValue<*> + + if (value != other.value) return false + return expiresAt == other.expiresAt + } + + override fun hashCode(): Int { + var result = value?.hashCode() ?: 0 + result = 31 * result + expiresAt.hashCode() + return result + } +} 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 74% 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 index ffa9a904e..722b06d41 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt @@ -6,6 +6,7 @@ import android.content.res.Configuration import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import java.util.concurrent.TimeUnit class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 { @@ -13,8 +14,8 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall application.registerComponentCallbacks(this) } - private val detailsCache = DeferredLruCache(4) - private val pagesCache = DeferredLruCache>(4) + private val detailsCache = ExpiringLruCache>(4, 5, TimeUnit.MINUTES) + private val pagesCache = ExpiringLruCache>>(4, 10, TimeUnit.MINUTES) override val isCachingEnabled: Boolean = true @@ -23,7 +24,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall } override fun putDetails(source: MangaSource, url: String, details: SafeDeferred) { - detailsCache.put(ContentCache.Key(source, url), details) + detailsCache[ContentCache.Key(source, url)] = details } override suspend fun getPages(source: MangaSource, url: String): List? { @@ -31,7 +32,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall } override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) { - pagesCache.put(ContentCache.Key(source, url), pages) + pagesCache[ContentCache.Key(source, url)] = pages } override fun onConfigurationChanged(newConfig: Configuration) = Unit @@ -43,17 +44,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall trimCache(pagesCache, level) } - private fun trimCache(cache: DeferredLruCache<*>, level: Int) { + private fun trimCache(cache: ExpiringLruCache<*>, level: Int) { when (level) { ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL, ComponentCallbacks2.TRIM_MEMORY_COMPLETE, - ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll() + ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.clear() ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN, ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1) - else -> cache.trimToSize(cache.maxSize() / 2) + else -> cache.trimToSize(cache.maxSize / 2) } } } 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 98% 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..5378e420e 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 @@ -33,6 +33,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,7 +47,6 @@ 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 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 86% 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 467fcab9a..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,10 +1,13 @@ package org.koitharu.kotatsu.core.db.entity import org.koitharu.kotatsu.core.model.MangaSource -import org.koitharu.kotatsu.parsers.model.* +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 @@ -66,7 +69,6 @@ fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching { SortOrder.valueOf(name) }.getOrDefault(fallback) -@Suppress("FunctionName") fun MangaState(name: String): MangaState? = runCatching { MangaState.valueOf(name) }.getOrNull() 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 100% 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 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/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 95% 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..b5e267dcb 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 @@ -12,13 +12,13 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog 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.core.util.isSuccess 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 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..03047a807 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.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 org.koitharu.kotatsu.util.ext.printStackTraceDebug 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 78% 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 4ade0b3a6..d63c2e3bb 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 @@ -4,17 +4,20 @@ import android.content.Context import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runInterruptible 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.util.ext.printStackTraceDebug import java.io.File import java.io.FileOutputStream import java.text.SimpleDateFormat @@ -82,6 +85,15 @@ class FileLogger( flushImpl() } + @WorkerThread + fun flushBlocking() { + if (!isEnabled) { + return + } + runBlockingSafe { flushJob?.cancelAndJoin() } + runBlockingSafe { flushImpl() } + } + private fun postFlush() { if (flushJob?.isActive == true) { return @@ -96,10 +108,10 @@ class FileLogger( } } - private suspend fun flushImpl() { + private suspend fun flushImpl() = withContext(NonCancellable) { mutex.withLock { if (buffer.isEmpty()) { - return + return@withContext } runInterruptible(Dispatchers.IO) { if (file.length() > MAX_SIZE_BYTES) { @@ -131,4 +143,9 @@ class FileLogger( } bakFile.delete() } + + private inline fun runBlockingSafe(crossinline block: suspend () -> Unit) = try { + runBlocking(NonCancellable) { block() } + } catch (_: InterruptedException) { + } } 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 50% 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 d1c3f849e..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,14 +1,17 @@ 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.parsers.util.toTitleCase -import org.koitharu.kotatsu.utils.ext.iterator fun Collection.ids() = mapToSet { it.id } +fun Collection.distinctById() = distinctBy { it.id } + fun Collection.countChaptersByBranch(): Int { if (size <= 1) { return size @@ -21,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()) { @@ -33,15 +40,25 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? { } } val groups = ch.groupBy { it.branch } + if (groups.size == 1) { + return groups.keys.first() + } + val candidates = HashMap>(groups.size) for (locale in LocaleListCompat.getAdjustedDefault()) { - var language = locale.getDisplayLanguage(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language - } - language = locale.getDisplayName(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language + val displayLanguage = locale.getDisplayLanguage(locale) + val displayName = locale.getDisplayName(locale) + for (branch in groups.keys) { + if (branch != null && ( + branch.contains(displayLanguage, ignoreCase = true) || + branch.contains(displayName, ignoreCase = true) + ) + ) { + candidates[branch] = groups[branch] ?: continue + } } } - return groups.maxByOrNull { it.value.size }?.key + 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..5beb6e42a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt @@ -0,0 +1,43 @@ +package org.koitharu.kotatsu.core.network + +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.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() { + + 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/kotlin/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt new file mode 100644 index 000000000..52710c57b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CacheLimitInterceptor.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.core.network + +import okhttp3.CacheControl +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit + +class CacheLimitInterceptor : Interceptor { + + private val defaultMaxAge = TimeUnit.HOURS.toSeconds(1) + private val defaultCacheControl = CacheControl.Builder() + .maxAge(defaultMaxAge.toInt(), TimeUnit.SECONDS) + .build() + .toString() + + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + val responseCacheControl = CacheControl.parse(response.headers) + if (responseCacheControl.noStore || responseCacheControl.maxAgeSeconds <= defaultMaxAge) { + return response + } + return response.newBuilder() + .header(CommonHeaders.CACHE_CONTROL, defaultCacheControl) + .build() + } +} 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 92% 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 943e08f2e..f8976acd6 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 @@ -13,6 +13,7 @@ object CommonHeaders { const val CONTENT_ENCODING = "Content-Encoding" const val ACCEPT_ENCODING = "Accept-Encoding" const val AUTHORIZATION = "Authorization" + const val CACHE_CONTROL = "Cache-Control" 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 92% 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 6896cc4ab..ce873fbc6 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,8 @@ 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.util.ext.printStackTraceDebug +import java.net.IDN import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +42,8 @@ class CommonHeadersInterceptor @Inject constructor( headersBuilder[CommonHeaders.USER_AGENT] = userAgentFallback } if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) { - headersBuilder.trySet(CommonHeaders.REFERER, "https://${repository.domain}/") + val idn = IDN.toASCII(repository.domain) + headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/") } val newRequest = request.newBuilder().headers(headersBuilder.build()).build() return repository?.intercept(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest) 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..9547b4da5 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.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/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt similarity index 94% 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 index 67da24e09..034a6513e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt @@ -11,6 +11,7 @@ import okhttp3.internal.closeQuietly import okhttp3.internal.publicsuffix.PublicSuffixDatabase 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.parsers.model.MangaSource import javax.inject.Inject import javax.inject.Singleton @@ -18,10 +19,14 @@ import javax.inject.Singleton @Singleton class MirrorSwitchInterceptor @Inject constructor( private val mangaRepositoryFactoryLazy: Lazy, + private val settings: AppSettings, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() + if (!settings.isMirrorSwitchingAvailable) { + return chain.proceed(request) + } return try { val response = chain.proceed(request) if (response.isFailed) { 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..a60c5db64 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -0,0 +1,88 @@ +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)) + 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/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..a7dc8a04a 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.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 97% 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..4a709f38b 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.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 86% 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 39708b1bd..4481c4038 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 @@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap import androidx.room.InvalidationTracker import coil.ImageLoader import coil.request.ImageRequest @@ -21,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.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.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import org.koitharu.kotatsu.utils.ext.requireBitmap -import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.util.ext.printStackTraceDebug import javax.inject.Inject import javax.inject.Singleton @@ -92,6 +93,14 @@ class ShortcutsUpdater @Inject constructor( return manager.maxShortcutCountPerActivity > 0 } + fun notifyMangaOpened(mangaId: Long) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return + } + val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + manager.reportShortcutUsed(mangaId.toString()) + } + @RequiresApi(Build.VERSION_CODES.N_MR1) private suspend fun updateShortcutsImpl() = runCatchingCancellable { val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager @@ -122,7 +131,7 @@ class ShortcutsUpdater @Inject constructor( .precision(Precision.EXACT) .scale(Scale.FILL) .build(), - ).requireBitmap() + ).getDrawableOrThrow().toBitmap() }.fold( onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }, 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 55% 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..45c2e5df9 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,28 @@ -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.network.MangaHttpClient 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, + @MangaHttpClient private val okHttpClient: OkHttpClient, private val db: MangaDatabase, ) { @@ -106,65 +91,10 @@ class MangaDataRepository @Inject constructor( } } - /** - * 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, ) - - 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 96% 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..36bbbc0c7 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaTagHighlighter.kt 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 96% 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..0f3d6d482 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, 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 90% 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 ab403a43c..be0d0dcfc 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 @@ -19,10 +19,13 @@ import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import okhttp3.internal.closeQuietly +import okio.Closeable +import okio.buffer import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.local.data.CacheDir +import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import java.net.HttpURLConnection @@ -54,7 +57,9 @@ class FaviconFetcher( val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" } val response = loadIcon(icon.url, mangaSource) val responseBody = response.requireBody() - val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource() + val source = writeToDiskCache(responseBody)?.toImageSource()?.also { + response.closeQuietly() + } ?: responseBody.toImageSource(response) return SourceResult( source = source, mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type), @@ -71,7 +76,7 @@ class FaviconFetcher( options.tags.asMap().forEach { request.tag(it.key as Class, it.value) } val response = okHttpClient.newCall(request.build()).await() if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) { - response.body?.closeQuietly() + response.closeQuietly() throw HttpException(response) } return response @@ -116,8 +121,12 @@ class FaviconFetcher( return ImageSource(data, fileSystem, diskCacheKey, this) } - private fun ResponseBody.toImageSource(): ImageSource { - return ImageSource(source(), options.context, FaviconMetadata(mangaSource)) + private fun ResponseBody.toImageSource(response: Closeable): ImageSource { + return ImageSource( + source().withExtraCloseable(response).buffer(), + options.context, + FaviconMetadata(mangaSource), + ) } private fun Response.toDataSource(): DataSource { 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 82% 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 471c3eece..e3a2e2b5c 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,16 +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.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.parsers.util.mapToSet +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 @@ -165,6 +169,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true) set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) } + val isMirrorSwitchingAvailable: Boolean + get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, true) + val isExitConfirmationEnabled: Boolean get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false) @@ -174,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) @@ -238,15 +249,28 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isDownloadsSlowdownEnabled: Boolean get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false) - val downloadsParallelism: Int - get() = prefs.getInt(KEY_DOWNLOADS_PARALLELISM, 2) + val isDownloadsWiFiOnly: Boolean + get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false) - val isSuggestionsEnabled: Boolean + var isSuggestionsEnabled: Boolean get() = prefs.getBoolean(KEY_SUGGESTIONS, false) + set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) } val isSuggestionsExcludeNsfw: Boolean get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false) + val isSuggestionsNotificationAvailable: Boolean + get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, true) + + val suggestionsTagsBlacklist: Set + get() { + val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') + if (string.isNullOrEmpty()) { + return emptySet() + } + return string.split(',').mapToSet { it.trim() } + } + val isReaderBarEnabled: Boolean get() = prefs.getBoolean(KEY_READER_BAR, true) @@ -259,6 +283,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { 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 + var localListOrder: SortOrder get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST) set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) } @@ -271,22 +307,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) - } - - fun getSuggestionsTagsBlacklistRegex(): Regex? { - val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') - if (string.isNullOrEmpty()) { - return null + 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) } - val tags = string.split(',') - val regex = tags.joinToString(prefix = "(", separator = "|", postfix = ")") { tag -> - Regex.escape(tag.trim()) - } - return Regex(regex, RegexOption.IGNORE_CASE) - } fun getMangaSources(includeHidden: Boolean): List { val list = remoteSources.toMutableList() @@ -324,6 +352,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" @@ -340,6 +376,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" + const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" const val KEY_COOKIES_CLEAR = "cookies_clear" const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear" const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear" @@ -378,17 +415,19 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SUGGESTIONS = "suggestions" const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw" const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags" + const val KEY_SUGGESTIONS_NOTIFICATIONS = "suggestions_notifications" const val KEY_SHIKIMORI = "shikimori" const val KEY_ANILIST = "anilist" const val KEY_MAL = "mal" const val KEY_KITSU = "kitsu" - const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism" const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" + const val KEY_DOWNLOADS_WIFI = "downloads_wifi" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" const val KEY_DOH = "doh" const val KEY_EXIT_CONFIRM = "exit_confirm" const val KEY_INCOGNITO_MODE = "incognito" const val KEY_SYNC = "sync" + const val KEY_SYNC_SETTINGS = "sync_settings" const val KEY_READER_BAR = "reader_bar" const val KEY_READER_SLIDER = "reader_slider" const val KEY_SHORTCUTS = "dynamic_shortcuts" @@ -405,6 +444,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_TIPS_CLOSED = "tips_closed" 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" // 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 54% 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..40c091d4d 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,13 +13,15 @@ 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) @@ -32,6 +35,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 +50,14 @@ abstract class AlertDialogFragment : DialogFragment() { open fun onDialogCreated(dialog: AlertDialog) = Unit - protected fun bindingOrNull(): B? = viewBinding + @Deprecated("", ReplaceWith("viewBinding")) + protected fun bindingOrNull() = viewBinding - protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B + 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 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 81% 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 234b1192c..f547e527a 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,7 +1,9 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.content.Intent import android.content.res.Configuration +import android.graphics.Color +import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.MenuItem @@ -23,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 : @@ -36,7 +38,7 @@ abstract class BaseActivity : private var isAmoledTheme = false - protected lateinit var binding: B + lateinit var viewBinding: B private set @JvmField @@ -48,6 +50,8 @@ abstract class BaseActivity : @JvmField val actionModeDelegate = ActionModeDelegate() + private var defaultStatusBarColor = Color.TRANSPARENT + override fun onCreate(savedInstanceState: Bundle?) { val settings = EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).settings isAmoledTheme = settings.isAmoledTheme @@ -84,7 +88,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) @@ -119,11 +123,15 @@ abstract class BaseActivity : override fun onSupportActionModeStarted(mode: ActionMode) { super.onSupportActionModeStarted(mode) actionModeDelegate.onSupportActionModeStarted(mode) - val actionModeColor = ColorUtils.compositeColors( - ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color), - getThemeColor(com.google.android.material.R.attr.colorSurface), - ) - val insets = ViewCompat.getRootWindowInsets(binding.root) + val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ColorUtils.compositeColors( + ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color), + getThemeColor(com.google.android.material.R.attr.colorSurface), + ) + } else { + ContextCompat.getColor(this, R.color.kotatsu_secondaryContainer) + } + val insets = ViewCompat.getRootWindowInsets(viewBinding.root) ?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return findViewById(androidx.appcompat.R.id.action_mode_bar).apply { setBackgroundColor(actionModeColor) @@ -131,6 +139,7 @@ abstract class BaseActivity : topMargin = insets.top } } + defaultStatusBarColor = window.statusBarColor window.statusBarColor = actionModeColor } @@ -138,7 +147,7 @@ abstract class BaseActivity : override fun onSupportActionModeFinished(mode: ActionMode) { super.onSupportActionModeFinished(mode) actionModeDelegate.onSupportActionModeFinished(mode) - window.statusBarColor = getThemeColor(android.R.attr.statusBarColor) + window.statusBarColor = defaultStatusBarColor } private fun putDataToExtras(intent: Intent?) { 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 73% 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..9c81104a1 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,19 @@ 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 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 +41,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 +58,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 +79,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 +95,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/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt similarity index 53% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt index 697016c9a..6dfdadf1d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.os.Bundle import android.view.LayoutInflater @@ -6,19 +6,21 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate -import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate @Suppress("LeakingThis") abstract class BaseFragment : Fragment(), WindowInsetsDelegate.WindowInsetsListener { - private var viewBinding: B? = null + var viewBinding: B? = null + private set + @Deprecated("", ReplaceWith("requireViewBinding()")) protected val binding: B - get() = checkNotNull(viewBinding) + get() = requireViewBinding() @JvmField protected val exceptionResolver = ExceptionResolver(this) @@ -29,19 +31,20 @@ abstract class BaseFragment : protected val actionModeDelegate: ActionModeDelegate get() = (requireActivity() as BaseActivity<*>).actionModeDelegate - override fun onCreateView( + final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val binding = onInflateView(inflater, container) + ): View { + 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) insetsDelegate.onViewCreated(view) + onViewBindingCreated(requireViewBinding(), savedInstanceState) } override fun onDestroyView() { @@ -50,7 +53,14 @@ abstract class BaseFragment : 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 onInflateView(inflater: LayoutInflater, container: ViewGroup?): B + protected abstract fun onCreateViewBinding(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/BaseFullscreenActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt index e43ca8877..96a240e55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFullscreenActivity.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui +package org.koitharu.kotatsu.core.ui import android.graphics.Color import android.os.Build @@ -56,4 +56,4 @@ abstract class BaseFullscreenActivity : } 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/BasePreferenceFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt similarity index 88% 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..bfffb7ab3 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,9 +9,9 @@ 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.core.ui.util.RecyclerViewOwner +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.settings.SettingsHeadersFragment import javax.inject.Inject @@ -56,6 +56,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : ) } + @Suppress("UsePropertyAccessSyntax") protected fun setTitle(title: CharSequence) { (parentFragment as? SettingsHeadersFragment)?.setTitle(title) ?: activity?.setTitle(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 57% 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..74948fcdb 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.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..dc1c59bd4 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.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/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt new file mode 100644 index 000000000..3199138e4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt @@ -0,0 +1,93 @@ +package org.koitharu.kotatsu.core.ui.dialog + +import android.content.Context +import android.content.DialogInterface +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.hannesdorfmann.adapterdelegates4.AdapterDelegate +import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager +import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter +import org.koitharu.kotatsu.R + +class RecyclerViewAlertDialog private constructor( + private val delegate: AlertDialog +) : DialogInterface by delegate { + + fun show() = delegate.show() + + class Builder(context: Context) { + + private val recyclerView = RecyclerView(context) + private val delegatesManager = AdapterDelegatesManager>() + private var items: List? = null + + private val delegate = MaterialAlertDialogBuilder(context) + .setView(recyclerView) + + init { + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.updatePadding( + top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing), + ) + recyclerView.clipToPadding = false + } + + fun setTitle(@StringRes titleResId: Int): Builder { + delegate.setTitle(titleResId) + return this + } + + fun setTitle(title: CharSequence): Builder { + delegate.setTitle(title) + return this + } + + fun setIcon(@DrawableRes iconId: Int): Builder { + delegate.setIcon(iconId) + return this + } + + fun setPositiveButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener, + ): Builder { + delegate.setPositiveButton(textId, listener) + return this + } + + fun setNegativeButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener? = null + ): Builder { + delegate.setNegativeButton(textId, listener) + return this + } + + fun setCancelable(isCancelable: Boolean): Builder { + delegate.setCancelable(isCancelable) + return this + } + + fun addAdapterDelegate(subject: AdapterDelegate>): Builder { + delegatesManager.addDelegate(subject) + return this + } + + fun setItems(list: List): Builder { + items = list + return this + } + + fun create(): RecyclerViewAlertDialog { + recyclerView.adapter = ListDelegationAdapter(delegatesManager).also { + it.items = items + } + return RecyclerViewAlertDialog(delegate.create()) + } + } +} 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/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt new file mode 100644 index 000000000..4d15077e1 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/TwoButtonsAlertDialog.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.core.ui.dialog + +import android.content.Context +import android.content.DialogInterface +import android.view.LayoutInflater +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.databinding.DialogTwoButtonsBinding + +class TwoButtonsAlertDialog private constructor( + private val delegate: AlertDialog +) : DialogInterface by delegate { + + fun show() = delegate.show() + + class Builder(context: Context) { + + private val binding = DialogTwoButtonsBinding.inflate(LayoutInflater.from(context)) + + private val delegate = MaterialAlertDialogBuilder(context) + .setView(binding.root) + + fun setTitle(@StringRes titleResId: Int): Builder { + binding.title.setText(titleResId) + return this + } + + fun setTitle(title: CharSequence): Builder { + binding.title.text = title + return this + } + + fun setIcon(@DrawableRes iconId: Int): Builder { + binding.icon.setImageResource(iconId) + return this + } + + fun setPositiveButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener, + ): Builder { + initButton(binding.button1, DialogInterface.BUTTON_POSITIVE, textId, listener) + return this + } + + fun setNegativeButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener? = null + ): Builder { + initButton(binding.button2, DialogInterface.BUTTON_NEGATIVE, textId, listener) + return this + } + + fun create(): TwoButtonsAlertDialog { + val dialog = delegate.create() + binding.root.tag = dialog + return TwoButtonsAlertDialog(dialog) + } + + private fun initButton( + button: MaterialButton, + which: Int, + @StringRes textId: Int, + listener: DialogInterface.OnClickListener?, + ) { + button.setText(textId) + button.isVisible = true + button.setOnClickListener { + val dialog = binding.root.tag as DialogInterface + listener?.onClick(dialog, which) + dialog.dismiss() + } + } + } +} 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 90% 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 f5019c152..f9d41fec8 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,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -8,19 +8,22 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) + if (recyclerView.hasPendingAdapterUpdates()) { + return + } val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() if (firstVisibleItemPosition == RecyclerView.NO_POSITION) { return } - if (firstVisibleItemPosition <= offsetTop) { - onScrolledToStart(recyclerView) - } val visibleItemCount = layoutManager.childCount val totalItemCount = layoutManager.itemCount if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) { onScrolledToEnd(recyclerView) } + if (firstVisibleItemPosition <= offsetTop) { + onScrolledToStart(recyclerView) + } } abstract fun onScrolledToStart(recyclerView: RecyclerView) 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 57% 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 f39b81d14..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,10 +1,10 @@ -package org.koitharu.kotatsu.base.ui.list +package org.koitharu.kotatsu.core.ui.list import android.view.View -interface OnListItemClickListener { +fun interface OnListItemClickListener { fun onItemClick(item: I, view: View) fun onItemLongClick(item: I, view: View) = false -} \ No newline at end of file +} 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/kotlin/org/koitharu/kotatsu/core/ui/list/ScrollListenerInvalidationObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ScrollListenerInvalidationObserver.kt new file mode 100644 index 000000000..5acc5862c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/ScrollListenerInvalidationObserver.kt @@ -0,0 +1,30 @@ +package org.koitharu.kotatsu.core.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/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 95% 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..2b62a6d49 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,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.list.fastscroll +package org.koitharu.kotatsu.core.ui.list.fastscroll import android.content.Context import android.util.AttributeSet @@ -42,4 +42,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 98% 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..d7eca512d 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 @@ -22,9 +22,9 @@ 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.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 @@ -98,6 +98,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) } @@ -176,10 +177,12 @@ class FastScroller @JvmOverloads constructor( setYPositions() return true } + MotionEvent.ACTION_MOVE -> { setYPositions() return true } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { requestDisallowInterceptTouchEvent(false) setHandleSelected(false) @@ -248,17 +251,20 @@ class FastScroller @JvmOverloads constructor( setMargins(0, marginTop, 0, marginBottom) } } + is CoordinatorLayout -> layoutParams = (layoutParams as CoordinatorLayout.LayoutParams).apply { height = LayoutParams.MATCH_PARENT anchorGravity = GravityCompat.END anchorId = recyclerViewId setMargins(0, marginTop, 0, marginBottom) } + is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { height = LayoutParams.MATCH_PARENT gravity = GravityCompat.END setMargins(0, marginTop, 0, marginBottom) } + is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply { height = 0 addRule(RelativeLayout.ALIGN_TOP, recyclerViewId) @@ -266,6 +272,7 @@ class FastScroller @JvmOverloads constructor( addRule(RelativeLayout.ALIGN_END, recyclerViewId) setMargins(0, marginTop, 0, marginBottom) } + else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") } 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/java/org/koitharu/kotatsu/base/ui/util/ActionModeDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt similarity index 97% 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..585f39e69 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 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 50% 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 04a332cd2..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,30 +1,21 @@ -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 -import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner -import org.koitharu.kotatsu.utils.ext.findActivity 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) if (handle != null) { snackbar.setAction(R.string.undo) { handle.reverseAsync() } } - (snackbarHost.context.findActivity() as? BottomNavOwner)?.let { - snackbar.anchorView = it.bottomNav - } snackbar.show() } } 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 70% 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..d3d6bc475 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.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 93% 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 124c1dea1..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 @@ -10,9 +10,7 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu open class ShrinkOnScrollBehavior : Behavior { - @Suppress("unused") constructor() : super() - @Suppress("unused") constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) override fun onStartNestedScroll( 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 97% 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..a85868857 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,4 +1,4 @@ -package org.koitharu.kotatsu.base.ui.util +package org.koitharu.kotatsu.core.ui.util import android.view.View import androidx.core.graphics.Insets 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 97% 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..354206ad4 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,10 +21,10 @@ 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 @@ -70,6 +70,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 97% 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..3ad0838ad 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 @@ -13,7 +13,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( @@ -149,9 +149,7 @@ class ChipsView @JvmOverloads constructor( if (title != other.title) 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 { 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 95% 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..b34a958bf 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 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/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt new file mode 100644 index 000000000..d06daa6ea --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CancellableSource.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.core.util + +import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive +import okio.Buffer +import okio.ForwardingSource +import okio.Source + +class CancellableSource( + private val job: Job?, + delegate: Source, +) : ForwardingSource(delegate) { + + override fun read(sink: Buffer, byteCount: Long): Long { + job?.ensureActive() + return super.read(sink, byteCount) + } +} 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/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..e14d2703c --- /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 93% 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..cf5645f1c 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 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 89% 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 8535fbed3..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,7 +1,8 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.content.Context import android.net.Uri +import android.widget.Toast import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import org.koitharu.kotatsu.BuildConfig @@ -84,6 +85,7 @@ class ShareHelper(private val context: Context) { fun shareLogs(loggers: Collection) { val intentBuilder = ShareCompat.IntentBuilder(context) .setType(TYPE_TEXT) + var hasLogs = false for (logger in loggers) { val logFile = logger.file if (!logFile.exists()) { @@ -91,8 +93,13 @@ class ShareHelper(private val context: Context) { } val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile) intentBuilder.addStream(uri) + hasLogs = true + } + if (hasLogs) { + intentBuilder.setChooserTitle(R.string.share_logs) + intentBuilder.startChooser() + } else { + Toast.makeText(context, R.string.nothing_here, Toast.LENGTH_SHORT).show() } - intentBuilder.setChooserTitle(R.string.share_logs) - intentBuilder.startChooser() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt index ee84cffb2..8fba053eb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/TaggedActivityResult.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils +package org.koitharu.kotatsu.core.util import android.app.Activity @@ -8,4 +8,4 @@ class TaggedActivityResult( ) val TaggedActivityResult.isSuccess: Boolean - get() = this.result == Activity.RESULT_OK \ No newline at end of file + get() = this.result == Activity.RESULT_OK diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt new file mode 100644 index 000000000..5748c79bb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Throttler.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.core.util + +import android.os.SystemClock + +class Throttler( + private val timeoutMs: Long, +) { + + private var lastTick = 0L + + fun throttle(): Boolean { + val now = SystemClock.elapsedRealtime() + return if (lastTick + timeoutMs <= now) { + lastTick = now + true + } else { + false + } + } + + fun reset() { + lastTick = 0L + } +} 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/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt new file mode 100644 index 000000000..95e7aaa4e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.core.util + +import android.annotation.SuppressLint +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.WorkRequest +import androidx.work.await +import androidx.work.impl.WorkManagerImpl +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@SuppressLint("RestrictedApi") +class WorkManagerHelper( + workManager: WorkManager, +) { + + private val workManagerImpl = workManager as WorkManagerImpl + + suspend fun deleteWork(id: UUID) = suspendCoroutine { cont -> + workManagerImpl.workTaskExecutor.executeOnTaskThread { + try { + workManagerImpl.workDatabase.workSpecDao().delete(id.toString()) + cont.resume(Unit) + } catch (e: Exception) { + cont.resumeWithException(e) + } + } + } + + suspend fun deleteWorks(ids: Collection) = suspendCoroutine { cont -> + workManagerImpl.workTaskExecutor.executeOnTaskThread { + try { + val db = workManagerImpl.workDatabase + db.runInTransaction { + for (id in ids) { + db.workSpecDao().delete(id.toString()) + } + } + cont.resume(Unit) + } catch (e: Exception) { + cont.resumeWithException(e) + } + } + } + + suspend fun getWorkInfosByTag(tag: String): List { + return workManagerImpl.getWorkInfosByTag(tag).await() + } + + suspend fun getFinishedWorkInfosByTag(tag: String): List { + val query = WorkQuery.Builder.fromTags(listOf(tag)) + .addStates(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.CANCELLED, WorkInfo.State.FAILED)) + .build() + return workManagerImpl.getWorkInfos(query).await() + } + + suspend fun getWorkInfoById(id: UUID): WorkInfo? { + return workManagerImpl.getWorkInfoById(id).await() + } + + suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult { + return workManagerImpl.updateWork(request).await() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt new file mode 100644 index 000000000..533c407a2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.core.util + +import android.annotation.SuppressLint +import android.content.Context +import androidx.lifecycle.asFlow +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.impl.foreground.SystemForegroundService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope + +/** + * Workaround for issue + * https://issuetracker.google.com/issues/270245927 + * https://issuetracker.google.com/issues/280504155 + */ +class WorkServiceStopHelper( + private val context: Context, +) { + + fun setup() { + processLifecycleScope.launch(Dispatchers.Default) { + WorkManager.getInstance(context) + .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING)) + .asFlow() + .map { it.isEmpty() } + .distinctUntilChanged() + .collectLatest { + if (it) { + delay(1_000) + stopWorkerService() + } + } + } + } + + @SuppressLint("RestrictedApi") + private fun stopWorkerService() { + SystemForegroundService.getInstance()?.stop() + } +} + 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 89% 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 966338125..02c0f3ba0 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 @@ -19,6 +19,7 @@ import android.provider.Settings import android.view.View import android.view.ViewPropertyAnimator import android.view.Window +import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IntegerRes import androidx.core.app.ActivityOptionsCompat @@ -40,6 +41,8 @@ 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.koitharu.kotatsu.util.ext.printStackTraceDebug import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import kotlin.math.roundToLong @@ -133,8 +136,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 } fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation( @@ -170,3 +173,18 @@ fun Context.findActivity(): Activity? = when (this) { is ContextWrapper -> baseContext.findActivity() else -> null } + +inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean { + return try { + block() + true + } catch (e: Exception) { + if (e.isWebViewUnavailable()) { + Toast.makeText(this, R.string.web_view_unavailable, Toast.LENGTH_LONG).show() + finishAfterTransition() + false + } else { + throw e + } + } +} 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 82% 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 f8a91c25e..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) @@ -37,11 +38,20 @@ fun ImageView.disposeImageRequest() { fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build()) -fun ImageResult.requireBitmap() = when (this) { - is SuccessResult -> drawable.toBitmap() +fun ImageResult.getDrawableOrThrow() = when (this) { + is SuccessResult -> drawable is ErrorResult -> throw throwable } +@Deprecated( + "", + ReplaceWith( + "getDrawableOrThrow().toBitmap()", + "androidx.core.graphics.drawable.toBitmap", + ), +) +fun ImageResult.requireBitmap() = getDrawableOrThrow().toBitmap() + fun ImageResult.toBitmapOrNull() = when (this) { is SuccessResult -> try { drawable.toBitmap() 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 72% 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 6f6513707..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,8 +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) @@ -45,3 +47,17 @@ inline fun Collection.filterToSet(predicate: (T) -> Boolean): Set { fun Sequence.toListSorted(comparator: Comparator): List { return toMutableList().apply { sortWith(comparator) } } + +fun List.takeMostFrequent(limit: Int): List { + val map = ArrayMap(size) + for (item in this) { + map[item] = map.getOrDefault(item, 0) + 1 + } + val entries = map.entries.sortedByDescending { it.value } + val count = minOf(limit, entries.size) + return buildList(count) { + repeat(count) { i -> + add(entries[i].key) + } + } +} 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 98% 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..632030e09 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 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 98% 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..ac98e2b48 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 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 72% 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 4c08eec67..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,13 +1,15 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.os.SystemClock import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow 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 @@ -43,3 +45,20 @@ fun Flow.throttle(timeoutMillis: (T) -> Long): Flow { fun StateFlow.requireValue(): T = checkNotNull(value) { "StateFlow value is null" } + +fun Flow>.flatten(): Flow = flow { + collect { value -> + for (item in value) { + emit(item) + } + } +} + +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..bfd2db25f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt @@ -0,0 +1,35 @@ +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.BuildConfig +import org.koitharu.kotatsu.core.util.Event + +fun Flow.observe(owner: LifecycleOwner, collector: FlowCollector) { + if (BuildConfig.DEBUG) { + require((this as? StateFlow)?.value !is Event<*>) + } + val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT + owner.lifecycleScope.launch(start = start) { + owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + 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 68% 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 dec45bde0..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 @@ -34,18 +34,21 @@ fun Fragment.addMenuProvider(provider: MenuProvider) { } @MainThread -suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner = suspendCancellableCoroutine { cont -> +suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner { val liveData = viewLifecycleOwnerLiveData - val observer = object : Observer { - override fun onChanged(value: LifecycleOwner?) { - if (value != null) { - liveData.removeObserver(this) - cont.resume(value) + liveData.value?.let { return it } + return suspendCancellableCoroutine { cont -> + val observer = object : Observer { + override fun onChanged(value: LifecycleOwner?) { + if (value != null) { + liveData.removeObserver(this) + cont.resume(value) + } } } - } - liveData.observeForever(observer) - cont.invokeOnCancellation { - liveData.removeObserver(observer) + liveData.observeForever(observer) + cont.invokeOnCancellation { + liveData.removeObserver(observer) + } } } 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 93% 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..f5a23453e 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,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt new file mode 100644 index 000000000..d41e0ba38 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.core.util.ext + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import okhttp3.ResponseBody +import okio.BufferedSink +import okio.Source +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) +} + +suspend fun Source.cancellable(): Source { + val job = currentCoroutineContext()[Job] + return CancellableSource(job, this) +} + +suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) { + writeAll(source.cancellable()) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt new file mode 100644 index 000000000..eef3a3b45 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Insets.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.util.ext + +import android.view.View +import androidx.core.graphics.Insets + +fun Insets.end(view: View): Int { + return if (view.isRtl) left else right +} + +fun Insets.start(view: View): Int { + return if (view.isRtl) right else left +} 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/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt index 61066cd5d..baf078b7f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/OtherExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Other.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext @Suppress("UNCHECKED_CAST") fun Class.castOrNull(obj: Any?): T? { 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/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt new file mode 100644 index 000000000..bc43c656e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.core.util.ext + +import androidx.annotation.FloatRange +import org.koitharu.kotatsu.parsers.util.levenshteinDistance +import org.koitharu.kotatsu.util.ext.printStackTraceDebug +import java.util.UUID + +inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String { + return if (this.isNullOrEmpty()) defaultValue() else this +} + +fun String.longHashCode(): Long { + var h = 1125899906842597L + val len: Int = this.length + for (i in 0 until len) { + h = 31 * h + this[i].code + } + return h +} + +fun String.toUUIDOrNull(): UUID? = try { + UUID.fromString(this) +} catch (e: IllegalArgumentException) { + e.printStackTraceDebug() + null +} + +/** + * @param threshold 0 = exact match + */ +fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean { + if (threshold == 0f) { + return equals(other, ignoreCase = true) + } + val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f) + return diff < threshold +} 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 96% 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..102b9bdb1 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,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import android.content.Context import android.graphics.Color 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 88% 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 e1f235652..3623d56d1 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,9 +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 @@ -54,7 +54,9 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is IOException -> getDisplayMessage(message, resources) ?: localizedMessage else -> localizedMessage -} ?: resources.getString(R.string.error_occurred) +}.ifNullOrEmpty { + resources.getString(R.string.error_occurred) +} private fun getDisplayMessage(msg: String?, resources: Resources): String? = when { msg.isNullOrEmpty() -> null @@ -81,14 +83,7 @@ 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 } 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 61% 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 95d55f40d..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 @@ -7,14 +7,11 @@ import android.view.View.MeasureSpec import android.view.ViewGroup 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() { @@ -27,32 +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) - } - } - fun View.hasGlobalPoint(x: Int, y: Int): Boolean { if (visibility != View.VISIBLE) { return false @@ -65,7 +36,7 @@ fun View.hasGlobalPoint(x: Int, y: Int): Boolean { fun View.measureHeight(): Int { val vh = height return if (vh == 0) { - measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) measuredHeight } else vh } @@ -73,7 +44,7 @@ fun View.measureHeight(): Int { fun View.measureWidth(): Int { val vw = width return if (vw == 0) { - measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) measuredWidth } else vw } @@ -105,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) { @@ -135,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() @@ -177,7 +119,6 @@ val View.parents: Sequence } } -@Suppress("unused") fun View.measureDimension(desiredSize: Int, measureSpec: Int): Int { var result: Int val specMode = MeasureSpec.getMode(measureSpec) @@ -192,3 +133,17 @@ fun View.measureDimension(desiredSize: Int, measureSpec: Int): Int { } return result } + +fun V.setChecked(checked: Boolean, animate: Boolean) where V : View, V : Checkable { + val skipAnimation = !animate && checked != isChecked + isChecked = checked + if (skipAnimation) { + jumpDrawablesToCurrentState() + } +} + +var View.isRtl: Boolean + get() = layoutDirection == View.LAYOUT_DIRECTION_RTL + set(value) { + layoutDirection = if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR + } 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 94% 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..27108dc26 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,4 +1,4 @@ -package org.koitharu.kotatsu.utils.ext +package org.koitharu.kotatsu.core.util.ext import androidx.annotation.MainThread import androidx.fragment.app.Fragment 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 81% 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 e53806080..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,9 +1,9 @@ -package org.koitharu.kotatsu.utils.progress +package org.koitharu.kotatsu.core.util.progress import androidx.annotation.AnyThread import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow -import org.koitharu.kotatsu.download.ui.service.PausingHandle +import org.koitharu.kotatsu.download.ui.worker.PausingHandle class PausingProgressJob

( job: Job, @@ -23,4 +23,4 @@ class PausingProgressJob

( @AnyThread fun resume() = pausingHandle.resume() -} \ No newline at end of file +} 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 76% 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 f998a5119..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 @@ -19,6 +19,9 @@ class TimeLeftEstimator { emptyTick() return } + if (lastTick?.value == value) { + return + } val tick = Tick(value, total, SystemClock.elapsedRealtime()) lastTick?.let { val ticksCount = value - it.value @@ -42,9 +45,14 @@ class TimeLeftEstimator { return if (eta < tooLargeTime) eta else NO_TIME } + fun getEta(): Long { + val etl = getEstimatedTimeLeft() + return if (etl == NO_TIME) NO_TIME else System.currentTimeMillis() + etl + } + private class Tick( - val value: Int, - val total: Int, - val time: Long, + @JvmField val value: Int, + @JvmField val total: Int, + @JvmField val time: Long, ) -} \ No newline at end of file +} 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/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt new file mode 100644 index 000000000..4d93a464c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/BranchComparator.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.details.domain + +import org.koitharu.kotatsu.details.ui.model.MangaBranch + +class BranchComparator : Comparator { + + override fun compare(o1: MangaBranch, o2: MangaBranch): Int = compareValues(o1.name, o2.name) +} 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..ad889e3ce 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.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/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt similarity index 89% 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..2e0929ae9 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,8 +5,8 @@ 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.ui.widgets.BottomSheetHeaderBar class ChaptersBottomSheetMediator( bottomSheet: View, @@ -32,6 +32,9 @@ class ChaptersBottomSheetMediator( override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { isEnabled = isExpanded + if (!isExpanded) { + unlock() + } } override fun onLayoutChange( 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 79% 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 c5d8ae675..07a6f8902 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 @@ -12,20 +12,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.download.ui.service.DownloadService 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.ReaderState -import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback -import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import kotlin.math.roundToInt class ChaptersFragment : @@ -38,17 +38,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, ) @@ -74,10 +74,6 @@ 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, @@ -95,11 +91,7 @@ class ChaptersFragment : override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean { return when (item.itemId) { R.id.action_save -> { - DownloadService.start( - binding.recyclerViewChapters, - viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false, - selectionController?.snapshot(), - ) + viewModel.download(selectionController?.snapshot()) mode.finish() true } @@ -113,7 +105,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() @@ -169,12 +161,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() @@ -190,7 +184,7 @@ class ChaptersFragment : } override fun onSelectionChanged(controller: ListSelectionController, count: Int) { - binding.recyclerViewChapters.invalidateItemDecorations() + requireViewBinding().recyclerViewChapters.invalidateItemDecorations() } override fun onWindowInsetsChanged(insets: Insets) = Unit @@ -198,10 +192,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 } @@ -211,6 +208,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..27a937c0c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ChaptersMapper.kt @@ -0,0 +1,60 @@ +package org.koitharu.kotatsu.details.ui + +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 + +fun mapChapters( + remoteManga: Manga?, + localManga: Manga?, + history: MangaHistory?, + newCount: Int, + branch: String?, +): List { + val remoteChapters = remoteManga?.getChapters(branch).orEmpty() + val localChapters = localManga?.getChapters(branch).orEmpty() + if (remoteChapters.isEmpty() && localChapters.isEmpty()) { + return emptyList() + } + 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, + ) + } + 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, + ) + } + } + 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 100% 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 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 53% 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 4e3e5f37b..64abbc9f8 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 @@ -1,12 +1,13 @@ package org.koitharu.kotatsu.details.ui import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.transition.Slide import android.transition.TransitionManager import android.view.Gravity -import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator @@ -17,33 +18,36 @@ import androidx.core.graphics.Insets import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding -import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch +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.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.ui.widgets.BottomSheetHeaderBar +import org.koitharu.kotatsu.core.util.ViewBadge +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 import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.HistoryInfo -import org.koitharu.kotatsu.download.ui.service.DownloadService +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.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat -import org.koitharu.kotatsu.utils.ext.textAndVisible +import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import javax.inject.Inject @AndroidEntryPoint @@ -51,10 +55,12 @@ class DetailsActivity : BaseActivity(), View.OnClickListener, BottomSheetHeaderBar.OnExpansionChangeListener, - NoModalBottomSheetOwner { + NoModalBottomSheetOwner, + View.OnLongClickListener, + PopupMenu.OnMenuItemClickListener { override val bsHeader: BottomSheetHeaderBar? - get() = binding.headerChapters + get() = viewBinding.headerChapters @Inject lateinit var shortcutsUpdater: ShortcutsUpdater @@ -71,28 +77,29 @@ class DetailsActivity : setDisplayHomeAsUpEnabled(true) setDisplayShowTitleEnabled(false) } - binding.buttonRead.setOnClickListener(this) - binding.buttonDropdown.setOnClickListener(this) - viewBadge = ViewBadge(binding.buttonRead, this) + viewBinding.buttonRead.setOnClickListener(this) + viewBinding.buttonRead.setOnLongClickListener(this) + viewBinding.buttonDropdown.setOnClickListener(this) + viewBadge = ViewBadge(viewBinding.buttonRead, this) - chaptersMenuProvider = if (binding.layoutBottom != null) { - val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom)) + chaptersMenuProvider = if (viewBinding.layoutBottom != null) { + val bsMediator = ChaptersBottomSheetMediator(checkNotNull(viewBinding.layoutBottom)) actionModeDelegate.addListener(bsMediator) - checkNotNull(binding.headerChapters).addOnExpansionChangeListener(bsMediator) - checkNotNull(binding.headerChapters).addOnLayoutChangeListener(bsMediator) + checkNotNull(viewBinding.headerChapters).addOnExpansionChangeListener(bsMediator) + checkNotNull(viewBinding.headerChapters).addOnLayoutChangeListener(bsMediator) onBackPressedDispatcher.addCallback(bsMediator) ChaptersMenuProvider(viewModel, bsMediator) } else { ChaptersMenuProvider(viewModel, null) } - 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 -> @@ -102,55 +109,78 @@ class DetailsActivity : }, ), ) - viewModel.onShowToast.observe(this) { + viewModel.onShowToast.observeEvent(this) { makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show() } viewModel.historyInfo.observe(this, ::onHistoryChanged) - viewModel.selectedBranchName.observe(this) { - binding.headerChapters?.subtitle = it - binding.textViewSubtitle?.textAndVisible = it + viewModel.selectedBranch.observe(this) { + viewBinding.headerChapters?.subtitle = it + viewBinding.textViewSubtitle?.textAndVisible = it } viewModel.isChaptersReversed.observe(this) { - binding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu() + viewBinding.headerChapters?.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.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) + viewBinding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider) } override fun onClick(v: View) { - val manga = viewModel.manga.value ?: return when (v.id) { - R.id.button_read -> { - val chapterId = viewModel.historyInfo.value?.history?.chapterId - if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { - showChapterMissingDialog(chapterId) - } else { - startActivity( - ReaderActivity.newIntent( - context = this, - manga = manga, - branch = viewModel.selectedBranchValue, - ), - ) - } + R.id.button_read -> openReader(isIncognitoMode = false) + R.id.button_dropdown -> showBranchPopupMenu() + } + } + + override fun onLongClick(v: View): Boolean = when (v.id) { + R.id.button_read -> { + val menu = PopupMenu(v.context, v) + menu.inflate(R.menu.popup_read) + menu.setOnMenuItemClickListener(this) + menu.setForceShowIcon(true) + menu.show() + true + } + + else -> false + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_incognito -> { + openReader(isIncognitoMode = true) + true } - R.id.button_dropdown -> showBranchPopupMenu() + 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 } } @@ -160,16 +190,16 @@ class DetailsActivity : } else { headerBar.removeMenuProvider(chaptersMenuProvider) } - 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) { @@ -182,17 +212,17 @@ 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) } } 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) @@ -207,100 +237,84 @@ 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.headerChapters?.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) { + private fun showBranchPopupMenu() { + var dialog: DialogInterface? = null + val listener = OnListItemClickListener { item, _ -> + viewModel.setSelectedBranch(item.name) + dialog?.dismiss() + } + dialog = RecyclerViewAlertDialog.Builder(this) + .addAdapterDelegate(branchAD(listener)) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .setTitle(R.string.translations) + .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 + if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { 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) { _, _ -> - DownloadService.start(binding.appbar, remoteManga, setOf(chapterId)) - } - setCancelable(true) - }.show() - } - - private fun showBranchPopupMenu() { - val menu = PopupMenu(this, binding.headerChapters ?: binding.buttonDropdown) - val currentBranch = viewModel.selectedBranchValue - for (branch in viewModel.branches.value ?: return) { - val item = menu.menu.add(R.id.group_branches, Menu.NONE, Menu.NONE, branch) - item.isChecked = branch == currentBranch - } - menu.menu.setGroupCheckable(R.id.group_branches, true, true) - menu.setOnMenuItemClickListener { item -> - viewModel.setSelectedBranch(item.title?.toString()) - true - } - menu.show() - } - - private fun resolveError(e: Throwable) { - lifecycleScope.launch { - if (exceptionResolver.resolve(e)) { - viewModel.reload() - } else if (viewModel.manga.value == null) { - Toast.makeText(this@DetailsActivity, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() - finishAfterTransition() + } else { + startActivity( + ReaderActivity.newIntent( + context = this, + manga = manga, + branch = viewModel.selectedBranchValue, + isIncognitoMode = isIncognitoMode, + ), + ) + if (isIncognitoMode) { + Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show() } } } - private fun isTabletLayout() = binding.layoutBottom == null + private fun isTabletLayout() = viewBinding.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.headerChapters } 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) } } 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 76% 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..dbd6318e3 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,21 +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.measureHeight +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.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.parsers.model.Manga @@ -43,16 +55,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 @@ -70,19 +72,19 @@ class DetailsFragment : 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) + viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) @@ -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()) } } @@ -267,7 +269,7 @@ class DetailsFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + requireViewBinding().root.updatePadding( bottom = ( (activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight() ?.plus(insets.bottom)?.plus(resources.resolveDp(16)) @@ -277,7 +279,7 @@ class DetailsFragment : } private fun bindTags(manga: Manga) { - binding.chipsTags.setChips( + requireViewBinding().chipsTags.setChips( manga.tags.map { tag -> ChipsView.ChipModel( title = tag.title, @@ -292,13 +294,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 90% 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 4592b4976..0f20ad3e0 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.download.ui.service.DownloadService +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.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.search.ui.multi.MultiSearchActivity -import org.koitharu.kotatsu.utils.ShareHelper class DetailsMenuProvider( private val activity: FragmentActivity, @@ -86,7 +86,7 @@ class DetailsMenuProvider( if (chaptersCount > 5 || branches.size > 1) { showSaveConfirmation(it, chaptersCount, branches) } else { - DownloadService.start(snackbarHost, it) + viewModel.download(null) } } } @@ -125,22 +125,24 @@ class DetailsMenuProvider( return true } - private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List) { + private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List) { val dialogBuilder = MaterialAlertDialogBuilder(activity) .setTitle(R.string.save_manga) .setNegativeButton(android.R.string.cancel, null) if (branches.size > 1) { - val items = Array(branches.size) { i -> branches[i].orEmpty() } - val currentBranch = viewModel.selectedBranchIndex.value ?: -1 + val items = Array(branches.size) { i -> branches[i].name.orEmpty() } + val currentBranch = branches.indexOfFirst { it.isSelected } val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch } dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked -> checkedIndices[i] = checked }.setPositiveButton(R.string.save) { _, _ -> - val selectedBranches = branches.filterIndexedTo(HashSet()) { i, _ -> checkedIndices[i] } + val selectedBranches = branches.mapIndexedNotNullTo(HashSet()) { i, b -> + if (checkedIndices[i]) b.name else null + } val chaptersIds = manga.chapters?.mapNotNullToSet { c -> if (c.branch in selectedBranches) c.id else null } - DownloadService.start(snackbarHost, manga, chaptersIds) + viewModel.download(chaptersIds) } } else { dialogBuilder.setMessage( @@ -149,7 +151,7 @@ class DetailsMenuProvider( activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount), ), ).setPositiveButton(R.string.save) { _, _ -> - DownloadService.start(snackbarHost, manga) + viewModel.download(null) } } dialogBuilder.show() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt new file mode 100644 index 000000000..7579b3301 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -0,0 +1,326 @@ +package org.koitharu.kotatsu.details.ui + +import android.text.Html +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import androidx.core.net.toUri +import androidx.core.text.getSpans +import androidx.core.text.parseAsHtml +import androidx.lifecycle.SavedStateHandle +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.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.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus +import org.koitharu.kotatsu.R +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.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.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.history.data.HistoryRepository +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.parsers.model.Manga +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 javax.inject.Inject + +@HiltViewModel +class DetailsViewModel @Inject constructor( + private val historyRepository: HistoryRepository, + private val bookmarksRepository: BookmarksRepository, + private val settings: AppSettings, + private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, + private val imageGetter: Html.ImageGetter, + @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 = MutableEventFlow() + val onDownloadStarted = MutableEventFlow() + + 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) + + val favouriteCategories = interactor.observeIsFavourite(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) + + val newChaptersCount = interactor.observeNewChapters(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) + + private val chaptersQuery = MutableStateFlow("") + val selectedBranch = MutableStateFlow(null) + + val isChaptersReversed = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, + key = AppSettings.KEY_REVERSE_CHAPTERS, + valueProducer = { chaptersReverse }, + ) + + val historyInfo: StateFlow = combine( + manga, + selectedBranch, + history, + interactor.observeIncognitoMode(manga), + ) { m, b, h, im -> + HistoryInfo(m, b, h, im) + }.stateIn( + scope = viewModelScope + Dispatchers.Default, + started = SharingStarted.Eagerly, + initialValue = HistoryInfo(null, null, null, false), + ) + + val bookmarks = manga.flatMapLatest { + if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) + }.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 = manga + .distinctUntilChangedBy { it?.description.orEmpty() } + .transformLatest { + val description = it?.description + if (description.isNullOrEmpty()) { + emit(null) + } else { + emit(description.parseAsHtml().filterSpans()) + emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans()) + } + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null) + + val onMangaRemoved = MutableEventFlow() + val isScrobblingAvailable: Boolean + get() = scrobblers.any { it.isAvailable } + + val scrobblingInfo: StateFlow> = interactor.observeScrobblingInfo(mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + + val branches: StateFlow> = combine( + doubleManga, + selectedBranch, + ) { m, b -> + 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()) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) + + val isChaptersEmpty: StateFlow = combine( + doubleManga, + isLoading, + ) { manga, loading -> + manga?.any != null && manga.chapters.isNullOrEmpty() && !loading + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + val chapters = combine( + combine( + doubleManga, + history, + selectedBranch, + newChaptersCount, + ) { manga, history, branch, news -> + mapChapters(manga?.remote, manga?.local, history, news, branch) + }, + isChaptersReversed, + chaptersQuery, + ) { list, reversed, query -> + (if (reversed) list.asReversed() else list).filterSearch(query) + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val selectedBranchValue: String? + get() = selectedBranch.value + + init { + loadingJob = doLoad() + launchJob(Dispatchers.Default) { + localStorageChanges + .collect { onDownloadComplete(it) } + } + } + + fun reload() { + loadingJob.cancel() + loadingJob = doLoad() + } + + fun deleteLocal() { + val m = doubleManga.value?.local + if (m == null) { + onShowToast.call(R.string.file_not_found) + return + } + launchLoadingJob(Dispatchers.Default) { + deleteLocalMangaUseCase(m) + onMangaRemoved.call(m) + } + } + + fun removeBookmark(bookmark: Bookmark) { + launchJob { + bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId) + onShowToast.call(R.string.bookmark_removed) + } + } + + fun setChaptersReversed(newValue: Boolean) { + settings.chaptersReverse = newValue + } + + fun setSelectedBranch(branch: String?) { + selectedBranch.value = branch + } + + fun performChapterSearch(query: String?) { + chaptersQuery.value = query?.trim().orEmpty() + } + + fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) { + val scrobbler = getScrobbler(index) ?: return + launchJob(Dispatchers.Default) { + scrobbler.updateScrobblingInfo( + mangaId = mangaId, + rating = rating, + status = status, + comment = null, + ) + } + } + + fun unregisterScrobbling(index: Int) { + val scrobbler = getScrobbler(index) ?: return + launchJob(Dispatchers.Default) { + scrobbler.unregisterScrobbling( + mangaId = mangaId, + ) + } + } + + fun markChapterAsCurrent(chapterId: Long) { + launchJob(Dispatchers.Default) { + 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.requireAny(), + chapterId = chapterId, + page = 0, + scroll = 0, + percent = percent, + ) + } + } + + fun download(chaptersIds: Set?) { + launchJob(Dispatchers.Default) { + downloadScheduler.schedule( + doubleManga.requireValue().requireAny(), + chaptersIds, + ) + onDownloadStarted.call(Unit) + } + } + + private fun doLoad() = launchLoadingJob(Dispatchers.Default) { + 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 { + if (query.isEmpty() || this.isEmpty()) { + return this + } + return filter { + it.chapter.name.contains(query, ignoreCase = true) + } + } + + private suspend fun onDownloadComplete(downloadedManga: LocalManga?) { + downloadedManga ?: return + launchJob { + doubleManga.update { + interactor.updateLocal(it, downloadedManga) + } + } + } + + private fun Spanned.filterSpans(): CharSequence { + val spannable = SpannableString.valueOf(this) + val spans = spannable.getSpans() + for (span in spans) { + spannable.removeSpan(span) + } + return spannable.trim() + } + + private fun getScrobbler(index: Int): Scrobbler? { + val info = scrobblingInfo.value.getOrNull(index) + val scrobbler = if (info != null) { + scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable } + } else { + null + } + if (scrobbler == null) { + errorEvent.call(IllegalStateException("Scrobbler [$index] is not available")) + } + return scrobbler + } +} 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/adapter/BranchAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt new file mode 100644 index 000000000..d471a5c0b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchAD.kt @@ -0,0 +1,39 @@ +package org.koitharu.kotatsu.details.ui.adapter + +import android.graphics.Color +import android.text.Spannable +import android.text.style.ForegroundColorSpan +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.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 + +fun branchAD( + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }, +) { + + val clickAdapter = AdapterDelegateClickListenerAdapter(this, clickListener) + itemView.setOnClickListener(clickAdapter) + val counterColorSpan = ForegroundColorSpan(context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY)) + val counterSizeSpan = RelativeSizeSpan(0.86f) + + bind { + binding.root.text = buildSpannedString { + append(item.name ?: getString(R.string.system_default)) + append(' ') + append(' ') + val start = length + append(item.count.toString()) + setSpan(counterColorSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + setSpan(counterSizeSpan, start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + binding.root.isChecked = item.isSelected + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt new file mode 100644 index 000000000..add2a583f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.details.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.details.ui.model.MangaBranch + +class BranchesAdapter( + list: List, + listener: OnListItemClickListener, +) : ListDelegationAdapter>() { + + init { + delegatesManager.addDelegate(branchAD(listener)) + items = list + } +} 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..6ff818ccf --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt @@ -0,0 +1,49 @@ +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.core.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +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.imageViewDownloaded.isVisible = item.isDownloaded + binding.imageViewNew.isVisible = item.isNew + } +} 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 81% 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..2a62acb1e 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,17 @@ 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 isNew: Boolean + get() = hasFlag(FLAG_NEW) fun description(): CharSequence? { val scanlator = chapter.scanlator?.takeUnless { it.isBlank() } @@ -38,6 +43,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 +55,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 +70,6 @@ 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_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 85% 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..73d70d3df 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 @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.model 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 +10,12 @@ fun MangaChapter.toListItem( isCurrent: Boolean, isUnread: Boolean, isNew: Boolean, - isMissing: Boolean, isDownloaded: 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 (isDownloaded) flags = flags or FLAG_DOWNLOADED return ChapterListItem( chapter = this, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt new file mode 100644 index 000000000..8588187f6 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.details.ui.model + +import org.koitharu.kotatsu.list.ui.model.ListModel + +class MangaBranch( + val name: String?, + val count: Int, + val isSelected: Boolean, +) : ListModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MangaBranch + + if (name != other.name) return false + if (count != other.count) return false + return isSelected == other.isSelected + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + count + result = 31 * result + isSelected.hashCode() + return result + } + + override fun toString(): String { + return "$name: $count" + } +} 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 89% 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..7b73a0277 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, 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/ScrobblingInfoBottomSheet.kt similarity index 67% 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/ScrobblingInfoBottomSheet.kt index dfaae62fd..045fb420d 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/ScrobblingInfoBottomSheet.kt @@ -17,18 +17,20 @@ 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.BaseBottomSheet +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.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 javax.inject.Inject @AndroidEntryPoint @@ -52,15 +54,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,7 +72,7 @@ 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) } @@ -83,7 +86,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 +98,7 @@ class ScrobblingInfoBottomSheet : viewModel.updateScrobbling( index = scrobblerIndex, rating = rating / ratingBar.numStars, - status = enumValues().getOrNull(binding.spinnerStatus.selectedItemPosition), + status = enumValues().getOrNull(requireViewBinding().spinnerStatus.selectedItemPosition), ) } } @@ -104,7 +107,7 @@ 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()) } @@ -117,13 +120,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 + 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 +137,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,7 +151,7 @@ class ScrobblingInfoBottomSheet : R.id.action_edit -> { val manga = viewModel.manga.value ?: return false - val scrobblerService = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.scrobbler + val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService) dismiss() } 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/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt new file mode 100644 index 000000000..8adf4a953 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt @@ -0,0 +1,123 @@ +package org.koitharu.kotatsu.download.domain + +import androidx.work.Data +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 + +data class DownloadState( + val manga: Manga, + val isIndeterminate: Boolean, + val isPaused: Boolean = false, + val isStopped: Boolean = false, + val error: String? = null, + val totalChapters: Int = 0, + val currentChapter: Int = 0, + val totalPages: Int = 0, + val currentPage: Int = 0, + val eta: Long = -1L, + val localManga: LocalManga? = null, + val downloadedChapters: LongArray = LongArray(0), + val timestamp: Long = System.currentTimeMillis(), +) { + + val max: Int = totalChapters * totalPages + + val progress: Int = totalPages * currentChapter + currentPage + 1 + + val percent: Float = if (max > 0) progress.toFloat() / max else PROGRESS_NONE + + val isFinalState: Boolean + get() = localManga != null || (error != null && !isPaused) + + val isParticularProgress: Boolean + get() = localManga == null && error == null && !isPaused && !isStopped && max > 0 && !isIndeterminate + + fun toWorkData() = Data.Builder() + .putLong(DATA_MANGA_ID, manga.id) + .putInt(DATA_MAX, max) + .putInt(DATA_PROGRESS, progress) + .putLong(DATA_ETA, eta) + .putLong(DATA_TIMESTAMP, timestamp) + .putString(DATA_ERROR, error) + .putLongArray(DATA_CHAPTERS, downloadedChapters) + .putBoolean(DATA_INDETERMINATE, isIndeterminate) + .putBoolean(DATA_PAUSED, isPaused) + .build() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DownloadState + + if (manga != other.manga) return false + if (isIndeterminate != other.isIndeterminate) return false + if (isPaused != other.isPaused) return false + if (isStopped != other.isStopped) return false + if (error != other.error) return false + if (totalChapters != other.totalChapters) return false + if (currentChapter != other.currentChapter) return false + if (totalPages != other.totalPages) return false + if (currentPage != other.currentPage) return false + if (eta != other.eta) return false + if (localManga != other.localManga) return false + if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false + if (timestamp != other.timestamp) return false + if (max != other.max) return false + if (progress != other.progress) return false + return percent == other.percent + } + + override fun hashCode(): Int { + var result = manga.hashCode() + result = 31 * result + isIndeterminate.hashCode() + result = 31 * result + isPaused.hashCode() + result = 31 * result + isStopped.hashCode() + result = 31 * result + (error?.hashCode() ?: 0) + result = 31 * result + totalChapters + result = 31 * result + currentChapter + result = 31 * result + totalPages + result = 31 * result + currentPage + result = 31 * result + eta.hashCode() + result = 31 * result + (localManga?.hashCode() ?: 0) + result = 31 * result + downloadedChapters.contentHashCode() + result = 31 * result + timestamp.hashCode() + result = 31 * result + max + result = 31 * result + progress + result = 31 * result + percent.hashCode() + return result + } + + companion object { + + private const val DATA_MANGA_ID = "manga_id" + private const val DATA_MAX = "max" + private const val DATA_PROGRESS = "progress" + private const val DATA_CHAPTERS = "chapter" + private const val DATA_ETA = "eta" + private const val DATA_TIMESTAMP = "timestamp" + private const val DATA_ERROR = "error" + private const val DATA_INDETERMINATE = "indeterminate" + private const val DATA_PAUSED = "paused" + + fun getMangaId(data: Data): Long = data.getLong(DATA_MANGA_ID, 0L) + + fun isIndeterminate(data: Data): Boolean = data.getBoolean(DATA_INDETERMINATE, false) + + fun isPaused(data: Data): Boolean = data.getBoolean(DATA_PAUSED, false) + + fun getMax(data: Data): Int = data.getInt(DATA_MAX, 0) + + fun getError(data: Data): String? = data.getString(DATA_ERROR) + + fun getProgress(data: Data): Int = data.getInt(DATA_PROGRESS, 0) + + fun getEta(data: Data): Long = data.getLong(DATA_ETA, -1L) + + fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L)) + + fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt new file mode 100644 index 000000000..e2150152a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -0,0 +1,140 @@ +package org.koitharu.kotatsu.download.ui.list + +import android.view.View +import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleOwner +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 + +fun downloadItemAD( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + listener: DownloadItemListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }, +) { + + val percentPattern = context.resources.getString(R.string.percent_string_pattern) + + val clickListener = object : View.OnClickListener, View.OnLongClickListener { + override fun onClick(v: View) { + when (v.id) { + R.id.button_cancel -> listener.onCancelClick(item) + R.id.button_resume -> listener.onResumeClick(item) + R.id.button_pause -> listener.onPauseClick(item) + else -> listener.onItemClick(item, v) + } + } + + override fun onLongClick(v: View): Boolean { + return listener.onItemLongClick(item, v) + } + } + binding.buttonCancel.setOnClickListener(clickListener) + binding.buttonPause.setOnClickListener(clickListener) + binding.buttonResume.setOnClickListener(clickListener) + itemView.setOnClickListener(clickListener) + itemView.setOnLongClickListener(clickListener) + + bind { payloads -> + binding.textViewTitle.text = item.manga.title + binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply { + placeholder(R.drawable.ic_placeholder) + fallback(R.drawable.ic_placeholder) + error(R.drawable.ic_error_placeholder) + allowRgb565(true) + source(item.manga.source) + enqueueWith(coil) + } + when (item.workState) { + WorkInfo.State.ENQUEUED, + WorkInfo.State.BLOCKED -> { + binding.textViewStatus.setText(R.string.queued) + binding.progressBar.isIndeterminate = false + binding.progressBar.isVisible = false + binding.progressBar.isEnabled = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + binding.buttonCancel.isVisible = true + binding.buttonResume.isVisible = false + binding.buttonPause.isVisible = false + } + + WorkInfo.State.RUNNING -> { + binding.textViewStatus.setText( + if (item.isPaused) R.string.paused else R.string.manga_downloading_, + ) + binding.progressBar.isIndeterminate = item.isIndeterminate + binding.progressBar.isVisible = true + binding.progressBar.max = item.max + binding.progressBar.isEnabled = !item.isPaused + binding.progressBar.setProgressCompat(item.progress, payloads.isNotEmpty()) + binding.textViewPercent.text = percentPattern.format((item.percent * 100f).format(1)) + binding.textViewPercent.isVisible = true + binding.textViewDetails.textAndVisible = item.getEtaString() + binding.buttonCancel.isVisible = true + binding.buttonResume.isVisible = item.isPaused + binding.buttonPause.isVisible = item.canPause + } + + WorkInfo.State.SUCCEEDED -> { + binding.textViewStatus.setText(R.string.download_complete) + binding.progressBar.isIndeterminate = false + binding.progressBar.isVisible = false + binding.progressBar.isEnabled = true + binding.textViewPercent.isVisible = false + if (item.totalChapters > 0) { + binding.textViewDetails.text = context.resources.getQuantityString( + R.plurals.chapters, + item.totalChapters, + item.totalChapters, + ) + binding.textViewDetails.isVisible = true + } else { + binding.textViewDetails.isVisible = false + } + binding.buttonCancel.isVisible = false + binding.buttonResume.isVisible = false + binding.buttonPause.isVisible = false + } + + WorkInfo.State.FAILED -> { + binding.textViewStatus.setText(R.string.error_occurred) + binding.progressBar.isIndeterminate = false + binding.progressBar.isVisible = false + binding.progressBar.isEnabled = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.textAndVisible = item.error + binding.buttonCancel.isVisible = false + binding.buttonResume.isVisible = false + binding.buttonPause.isVisible = false + } + + WorkInfo.State.CANCELLED -> { + binding.textViewStatus.setText(R.string.canceled) + binding.progressBar.isIndeterminate = false + binding.progressBar.isVisible = false + binding.progressBar.isEnabled = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + binding.buttonCancel.isVisible = false + binding.buttonResume.isVisible = false + binding.buttonPause.isVisible = false + } + } + } + + onViewRecycled { + binding.imageViewCover.disposeImageRequest() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt new file mode 100644 index 000000000..d72a541c2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.download.ui.list + +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener + +interface DownloadItemListener : OnListItemClickListener { + + fun onCancelClick(item: DownloadItemModel) + + fun onPauseClick(item: DownloadItemModel) + + fun onResumeClick(item: DownloadItemModel) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt new file mode 100644 index 000000000..f8d95a66c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt @@ -0,0 +1,83 @@ +package org.koitharu.kotatsu.download.ui.list + +import android.text.format.DateUtils +import androidx.work.WorkInfo +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.Manga +import java.util.Date +import java.util.UUID + +class DownloadItemModel( + val id: UUID, + val workState: WorkInfo.State, + val isIndeterminate: Boolean, + val isPaused: Boolean, + val manga: Manga, + val error: String?, + val max: Int, + val totalChapters: Int, + val progress: Int, + val eta: Long, + val timestamp: Date, +) : ListModel, Comparable { + + val percent: Float + get() = if (max > 0) progress / max.toFloat() else 0f + + val hasEta: Boolean + get() = workState == WorkInfo.State.RUNNING && !isPaused && eta > 0L + + val canPause: Boolean + get() = workState == WorkInfo.State.RUNNING && !isPaused && error == null + + val canResume: Boolean + get() = workState == WorkInfo.State.RUNNING && isPaused + + fun getEtaString(): CharSequence? = if (hasEta) { + DateUtils.getRelativeTimeSpanString( + eta, + System.currentTimeMillis(), + DateUtils.SECOND_IN_MILLIS, + ) + } else { + null + } + + override fun compareTo(other: DownloadItemModel): Int { + return timestamp.compareTo(other.timestamp) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DownloadItemModel + + if (id != other.id) return false + if (workState != other.workState) return false + if (isIndeterminate != other.isIndeterminate) return false + if (isPaused != other.isPaused) return false + if (manga != other.manga) return false + if (error != other.error) return false + if (max != other.max) return false + if (totalChapters != other.totalChapters) return false + if (progress != other.progress) return false + if (eta != other.eta) return false + return timestamp == other.timestamp + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + workState.hashCode() + result = 31 * result + isIndeterminate.hashCode() + result = 31 * result + isPaused.hashCode() + result = 31 * result + manga.hashCode() + result = 31 * result + (error?.hashCode() ?: 0) + result = 31 * result + max + result = 31 * result + totalChapters + result = 31 * result + progress + result = 31 * result + eta.hashCode() + result = 31 * result + timestamp.hashCode() + return result + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt new file mode 100644 index 000000000..5a6128b75 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt @@ -0,0 +1,175 @@ +package org.koitharu.kotatsu.download.ui.list + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.viewModels +import androidx.annotation.Px +import androidx.appcompat.view.ActionMode +import androidx.core.graphics.Insets +import androidx.core.view.updatePadding +import coil.ImageLoader +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.FlowCollector +import org.koitharu.kotatsu.R +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 +import javax.inject.Inject + +@AndroidEntryPoint +class DownloadsActivity : BaseActivity(), + DownloadItemListener, + ListSelectionController.Callback2 { + + @Inject + lateinit var coil: ImageLoader + + private val viewModel by viewModels() + private lateinit var selectionController: ListSelectionController + + @Px + private var listSpacing = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityDownloadsBinding.inflate(layoutInflater)) + listSpacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val downloadsAdapter = DownloadsAdapter(this, coil, this) + val decoration = SpacingItemDecoration(listSpacing) + selectionController = ListSelectionController( + activity = this, + decoration = DownloadsSelectionDecoration(this), + registryOwner = this, + callback = this, + ) + with(viewBinding.recyclerView) { + setHasFixedSize(true) + addItemDecoration(decoration) + adapter = downloadsAdapter + selectionController.attachToRecyclerView(this) + } + addMenuProvider(DownloadsMenuProvider(this, viewModel)) + viewModel.items.observe(this) { + downloadsAdapter.items = it + } + 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) { + viewBinding.recyclerView.updatePadding( + left = insets.left + listSpacing, + right = insets.right + listSpacing, + bottom = insets.bottom, + ) + viewBinding.toolbar.updatePadding( + left = insets.left, + right = insets.right, + ) + } + + override fun onItemClick(item: DownloadItemModel, view: View) { + if (selectionController.onItemClick(item.id.mostSignificantBits)) { + return + } + startActivity(DetailsActivity.newIntent(view.context, item.manga)) + } + + override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean { + return selectionController.onItemLongClick(item.id.mostSignificantBits) + } + + override fun onCancelClick(item: DownloadItemModel) { + viewModel.cancel(item.id) + } + + override fun onPauseClick(item: DownloadItemModel) { + sendBroadcast(PausingReceiver.getPauseIntent(item.id)) + } + + override fun onResumeClick(item: DownloadItemModel) { + sendBroadcast(PausingReceiver.getResumeIntent(item.id)) + } + + override fun onSelectionChanged(controller: ListSelectionController, count: Int) { + viewBinding.recyclerView.invalidateItemDecorations() + } + + override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.mode_downloads, menu) + return true + } + + override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_resume -> { + viewModel.resume(controller.snapshot()) + mode.finish() + true + } + + R.id.action_pause -> { + viewModel.pause(controller.snapshot()) + mode.finish() + true + } + + R.id.action_cancel -> { + viewModel.cancel(controller.snapshot()) + mode.finish() + true + } + + R.id.action_remove -> { + viewModel.remove(controller.snapshot()) + mode.finish() + true + } + + R.id.action_select_all -> { + controller.addAll(viewModel.allIds()) + true + } + + else -> false + } + } + + override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { + val snapshot = viewModel.snapshot(controller.peekCheckedIds()) + var canPause = true + var canResume = true + var canCancel = true + var canRemove = true + for (item in snapshot) { + canPause = canPause and item.canPause + canResume = canResume and item.canResume + canCancel = canCancel and !item.workState.isFinished + canRemove = canRemove and item.workState.isFinished + } + menu.findItem(R.id.action_pause)?.isVisible = canPause + menu.findItem(R.id.action_resume)?.isVisible = canResume + menu.findItem(R.id.action_cancel)?.isVisible = canCancel + menu.findItem(R.id.action_remove)?.isVisible = canRemove + return super.onPrepareActionMode(controller, mode, menu) + } + + companion object { + + fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt new file mode 100644 index 000000000..16fa29387 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsAdapter.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.download.ui.list + +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +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 +import org.koitharu.kotatsu.list.ui.model.ListModel +import kotlin.jvm.internal.Intrinsics + +class DownloadsAdapter( + lifecycleOwner: LifecycleOwner, + coil: ImageLoader, + listener: DownloadItemListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(ITEM_TYPE_DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener)) + .addDelegate(loadingStateAD()) + .addDelegate(emptyStateListAD(coil, lifecycleOwner, null)) + .addDelegate(relatedDateItemAD()) + } + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when { + + oldItem is DownloadItemModel && newItem is DownloadItemModel -> { + oldItem.id == newItem.id + } + + oldItem is DateTimeAgo && newItem is DateTimeAgo -> { + oldItem == newItem + } + + else -> oldItem.javaClass == newItem.javaClass + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return Intrinsics.areEqual(oldItem, newItem) + } + + override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { + return when (newItem) { + is DownloadItemModel -> { + oldItem as DownloadItemModel + if (oldItem.workState == newItem.workState) { + Unit + } else { + null + } + } + + else -> super.getChangePayload(oldItem, newItem) + } + } + } + + companion object { + const val ITEM_TYPE_DOWNLOAD = 0 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt new file mode 100644 index 000000000..89428502e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt @@ -0,0 +1,63 @@ +package org.koitharu.kotatsu.download.ui.list + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R + +class DownloadsMenuProvider( + private val context: Context, + private val viewModel: DownloadsViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_downloads, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.action_pause -> viewModel.pauseAll() + R.id.action_resume -> viewModel.resumeAll() + R.id.action_cancel_all -> confirmCancelAll() + R.id.action_remove_completed -> confirmRemoveCompleted() + else -> return false + } + return true + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.findItem(R.id.action_pause)?.isVisible = viewModel.hasActiveWorks.value == true + menu.findItem(R.id.action_resume)?.isVisible = viewModel.hasPausedWorks.value == true + menu.findItem(R.id.action_cancel_all)?.isVisible = viewModel.hasCancellableWorks.value == true + } + + private fun confirmCancelAll() { + MaterialAlertDialogBuilder( + context, + com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered, + ).setTitle(R.string.cancel_all) + .setMessage(R.string.cancel_all_downloads_confirm) + .setIcon(R.drawable.ic_cancel_multiple) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.confirm) { _, _ -> + viewModel.cancelAll() + }.show() + } + + private fun confirmRemoveCompleted() { + MaterialAlertDialogBuilder( + context, + com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered, + ).setTitle(R.string.remove_completed) + .setMessage(R.string.remove_completed_downloads_confirm) + .setIcon(R.drawable.ic_clear_all) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewModel.removeCompleted() + }.show() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt new file mode 100644 index 000000000..eb47bc515 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsSelectionDecoration.kt @@ -0,0 +1,75 @@ +package org.koitharu.kotatsu.download.ui.list + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.view.View +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +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.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() { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle) + private val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_offset) + private val iconSize = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_size) + 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, + ) + private val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner) + + init { + hasBackground = false + hasForeground = true + isIncludeDecorAndMargins = false + + paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width) + checkIcon?.setTint(strokeColor) + } + + override fun getItemId(parent: RecyclerView, child: View): Long { + val holder = parent.getChildViewHolder(child) ?: return NO_ID + val item = holder.getItem(DownloadItemModel::class.java) ?: return NO_ID + return item.id.mostSignificantBits + } + + override fun onDrawForeground( + canvas: Canvas, + parent: RecyclerView, + child: View, + bounds: RectF, + state: RecyclerView.State, + ) { + val isCard = child is CardView + val radius = (child as? CardView)?.radius ?: defaultRadius + paint.color = fillColor + paint.style = Paint.Style.FILL + canvas.drawRoundRect(bounds, radius, radius, paint) + paint.color = strokeColor + paint.style = Paint.Style.STROKE + canvas.drawRoundRect(bounds, radius, radius, paint) + if (isCard) { + checkIcon?.run { + setBounds( + (bounds.right - iconSize - iconOffset).toInt(), + (bounds.top + iconOffset).toInt(), + (bounds.right - iconOffset).toInt(), + (bounds.top + iconOffset + iconSize).toInt(), + ) + draw(canvas) + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt new file mode 100644 index 000000000..24b3dc68f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -0,0 +1,246 @@ +package org.koitharu.kotatsu.download.ui.list + +import androidx.collection.LongSparseArray +import androidx.collection.getOrElse +import androidx.collection.set +import androidx.lifecycle.viewModelScope +import androidx.work.Data +import androidx.work.WorkInfo +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.koitharu.kotatsu.R +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 +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 java.util.Date +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class DownloadsViewModel @Inject constructor( + private val workScheduler: DownloadWorker.Scheduler, + private val mangaDataRepository: MangaDataRepository, +) : BaseViewModel() { + + private val mangaCache = LongSparseArray() + private val cacheMutex = Mutex() + private val works = workScheduler.observeWorks() + .mapLatest { it.toDownloadsList() } + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) + + val onActionDone = MutableEventFlow() + + val items = works.map { + it?.toUiList() ?: listOf(LoadingState) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) + + val hasPausedWorks = works.map { + it?.any { x -> x.canResume } == true + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) + + val hasActiveWorks = works.map { + it?.any { x -> x.canPause } == true + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) + + val hasCancellableWorks = works.map { + it?.any { x -> !x.workState.isFinished } == true + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), false) + + fun cancel(id: UUID) { + launchJob(Dispatchers.Default) { + workScheduler.cancel(id) + } + } + + fun cancel(ids: Set) { + launchJob(Dispatchers.Default) { + val snapshot = works.value ?: return@launchJob + for (work in snapshot) { + if (work.id.mostSignificantBits in ids) { + workScheduler.cancel(work.id) + } + } + onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null)) + } + } + + fun cancelAll() { + launchJob(Dispatchers.Default) { + workScheduler.cancelAll() + onActionDone.call(ReversibleAction(R.string.downloads_cancelled, null)) + } + } + + fun pause(ids: Set) { + val snapshot = works.value ?: return + for (work in snapshot) { + if (work.id.mostSignificantBits in ids) { + workScheduler.pause(work.id) + } + } + onActionDone.call(ReversibleAction(R.string.downloads_paused, null)) + } + + fun pauseAll() { + val snapshot = works.value ?: return + var isPaused = false + for (work in snapshot) { + if (work.canPause) { + workScheduler.pause(work.id) + isPaused = true + } + } + if (isPaused) { + onActionDone.call(ReversibleAction(R.string.downloads_paused, null)) + } + } + + fun resumeAll() { + val snapshot = works.value ?: return + var isResumed = false + for (work in snapshot) { + if (work.workState == WorkInfo.State.RUNNING && work.isPaused) { + workScheduler.resume(work.id) + isResumed = true + } + } + if (isResumed) { + onActionDone.call(ReversibleAction(R.string.downloads_resumed, null)) + } + } + + fun resume(ids: Set) { + val snapshot = works.value ?: return + for (work in snapshot) { + if (work.id.mostSignificantBits in ids) { + workScheduler.resume(work.id) + } + } + onActionDone.call(ReversibleAction(R.string.downloads_resumed, null)) + } + + fun remove(ids: Set) { + launchJob(Dispatchers.Default) { + val snapshot = works.value ?: return@launchJob + for (work in snapshot) { + if (work.id.mostSignificantBits in ids) { + workScheduler.delete(work.id) + } + } + onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) + } + } + + fun removeCompleted() { + launchJob(Dispatchers.Default) { + workScheduler.removeCompleted() + onActionDone.call(ReversibleAction(R.string.downloads_removed, null)) + } + } + + fun snapshot(ids: Set): Collection { + return works.value?.filterTo(ArrayList(ids.size)) { x -> x.id.mostSignificantBits in ids }.orEmpty() + } + + fun allIds(): Set = works.value?.mapToSet { + it.id.mostSignificantBits + } ?: emptySet() + + private suspend fun List.toDownloadsList(): List { + if (isEmpty()) { + return emptyList() + } + val list = mapNotNullTo(ArrayList(size)) { it.toUiModel() } + list.sortByDescending { it.timestamp } + return list + } + + private fun List.toUiList(): List { + if (isEmpty()) { + return emptyStateList() + } + val destination = ArrayList((size * 1.4).toInt()) + var prevDate: DateTimeAgo? = null + for (item in this) { + val date = timeAgo(item.timestamp) + if (prevDate != date) { + destination += date + } + prevDate = date + destination += item + } + return destination + } + + private suspend fun WorkInfo.toUiModel(): DownloadItemModel? { + val workData = if (outputData == Data.EMPTY) progress else outputData + val mangaId = DownloadState.getMangaId(workData) + if (mangaId == 0L) return null + val manga = getManga(mangaId) ?: return null + return DownloadItemModel( + id = id, + workState = state, + manga = manga, + error = DownloadState.getError(workData), + isIndeterminate = DownloadState.isIndeterminate(workData), + isPaused = DownloadState.isPaused(workData), + max = DownloadState.getMax(workData), + progress = DownloadState.getProgress(workData), + eta = DownloadState.getEta(workData), + timestamp = DownloadState.getTimestamp(workData), + totalChapters = DownloadState.getDownloadedChapters(workData).size, + ) + } + + private fun timeAgo(date: Date): DateTimeAgo { + val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L) + val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt() + val diffDays = -date.daysDiff(System.currentTimeMillis()) + return when { + diffMinutes < 3 -> DateTimeAgo.JustNow + diffDays < 1 -> DateTimeAgo.Today + diffDays == 1 -> DateTimeAgo.Yesterday + diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays) + else -> DateTimeAgo.Absolute(date) + } + } + + private fun emptyStateList() = listOf( + EmptyState( + icon = R.drawable.ic_empty_common, + textPrimary = R.string.text_downloads_list_holder, + textSecondary = 0, + actionStringRes = 0, + ), + ) + + private suspend fun getManga(mangaId: Long): Manga? { + mangaCache[mangaId]?.let { + return it + } + return cacheMutex.withLock { + mangaCache.getOrElse(mangaId) { + mangaDataRepository.findMangaById(mangaId)?.also { mangaCache[mangaId] = it } ?: return null + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt new file mode 100644 index 000000000..d7985849c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -0,0 +1,270 @@ +package org.koitharu.kotatsu.download.ui.worker + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Build +import android.text.format.DateUtils +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.work.WorkManager +import coil.ImageLoader +import coil.request.ImageRequest +import coil.size.Scale +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +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 +import org.koitharu.kotatsu.parsers.model.Manga +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.util.ext.printStackTraceDebug +import java.util.UUID +import com.google.android.material.R as materialR + +private const val CHANNEL_ID = "download" +private const val GROUP_ID = "downloads" + +class DownloadNotificationFactory @AssistedInject constructor( + @ApplicationContext private val context: Context, + private val coil: ImageLoader, + @Assisted private val uuid: UUID, +) { + + private val covers = HashMap() + private val builder = NotificationCompat.Builder(context, CHANNEL_ID) + private val mutex = Mutex() + + private val coverWidth = context.resources.getDimensionPixelSize( + androidx.core.R.dimen.compat_notification_large_icon_max_width, + ) + private val coverHeight = context.resources.getDimensionPixelSize( + androidx.core.R.dimen.compat_notification_large_icon_max_height, + ) + private val queueIntent = PendingIntentCompat.getActivity( + context, + 0, + DownloadsActivity.newIntent(context), + 0, + false, + ) + + private val actionCancel by lazy { + NotificationCompat.Action( + materialR.drawable.material_ic_clear_black_24dp, + context.getString(android.R.string.cancel), + WorkManager.getInstance(context).createCancelPendingIntent(uuid), + ) + } + + private val actionPause by lazy { + NotificationCompat.Action( + R.drawable.ic_action_pause, + context.getString(R.string.pause), + PausingReceiver.createPausePendingIntent(context, uuid), + ) + } + + private val actionResume by lazy { + NotificationCompat.Action( + R.drawable.ic_action_resume, + context.getString(R.string.resume), + PausingReceiver.createResumePendingIntent(context, uuid), + ) + } + + init { + createChannel() + builder.setOnlyAlertOnce(true) + builder.setDefaults(0) + builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + builder.setSilent(true) + builder.setGroup(GROUP_ID) + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + } + + suspend fun create(state: DownloadState?): Notification = mutex.withLock { + if (state == null) { + builder.setContentTitle(context.getString(R.string.manga_downloading_)) + builder.setContentText(context.getString(R.string.preparing_)) + } else { + builder.setContentTitle(state.manga.title) + builder.setContentText(context.getString(R.string.manga_downloading_)) + } + builder.setProgress(1, 0, true) + builder.setSmallIcon(android.R.drawable.stat_sys_download) + builder.setContentIntent(queueIntent) + builder.setStyle(null) + builder.setLargeIcon(if (state != null) getCover(state.manga)?.toBitmap() else null) + builder.clearActions() + builder.setSubText(null) + builder.setShowWhen(false) + builder.setVisibility( + if (state != null && state.manga.isNsfw) { + NotificationCompat.VISIBILITY_PRIVATE + } else { + NotificationCompat.VISIBILITY_PUBLIC + }, + ) + when { + state == null -> Unit + state.localManga != null -> { // downloaded, final state + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.download_complete)) + builder.setContentIntent(createMangaIntent(context, state.localManga.manga)) + builder.setAutoCancel(true) + builder.setSmallIcon(android.R.drawable.stat_sys_download_done) + builder.setCategory(null) + builder.setStyle(null) + builder.setOngoing(false) + builder.setShowWhen(true) + builder.setWhen(System.currentTimeMillis()) + } + + state.isStopped -> { + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.queued)) + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setStyle(null) + builder.setOngoing(true) + builder.setSmallIcon(R.drawable.ic_stat_paused) + builder.addAction(actionCancel) + } + + state.isPaused -> { // paused (with error or manually) + builder.setProgress(state.max, state.progress, false) + val percent = if (state.percent >= 0) { + context.getString(R.string.percent_string_pattern, (state.percent * 100).format()) + } else { + null + } + if (state.error != null) { + builder.setContentText(context.getString(R.string.download_summary_pattern, percent, state.error)) + } else { + builder.setContentText(percent) + } + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setStyle(null) + builder.setOngoing(true) + builder.setSmallIcon(R.drawable.ic_stat_paused) + builder.addAction(actionCancel) + builder.addAction(actionResume) + } + + state.error != null -> { // error, final state + builder.setProgress(0, 0, false) + builder.setSmallIcon(android.R.drawable.stat_notify_error) + builder.setSubText(context.getString(R.string.error)) + builder.setContentText(state.error) + builder.setAutoCancel(true) + builder.setOngoing(false) + builder.setCategory(NotificationCompat.CATEGORY_ERROR) + builder.setShowWhen(true) + builder.setWhen(System.currentTimeMillis()) + builder.setStyle(NotificationCompat.BigTextStyle().bigText(state.error)) + } + + else -> { + builder.setProgress(state.max, state.progress, false) + builder.setContentText(getProgressString(state.percent, state.eta)) + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setStyle(null) + builder.setOngoing(true) + builder.addAction(actionCancel) + builder.addAction(actionPause) + } + } + return builder.build() + } + + private fun getProgressString(percent: Float, eta: Long): CharSequence? { + val percentString = if (percent >= 0f) { + context.getString(R.string.percent_string_pattern, (percent * 100).format()) + } else { + null + } + val etaString = if (eta > 0L) { + DateUtils.getRelativeTimeSpanString( + eta, + System.currentTimeMillis(), + DateUtils.SECOND_IN_MILLIS, + ) + } else { + null + } + return when { + percentString == null && etaString == null -> null + percentString != null && etaString == null -> percentString + percentString == null && etaString != null -> etaString + else -> context.getString(R.string.download_summary_pattern, percentString, etaString) + } + } + + private fun createMangaIntent(context: Context, manga: Manga?) = PendingIntentCompat.getActivity( + context, + manga.hashCode(), + if (manga != null) { + DetailsActivity.newIntent(context, manga) + } else { + MangaListActivity.newIntent(context, MangaSource.LOCAL) + }, + PendingIntent.FLAG_CANCEL_CURRENT, + false, + ) + + private suspend fun getCover(manga: Manga) = covers[manga] ?: run { + runCatchingCancellable { + coil.execute( + ImageRequest.Builder(context) + .data(manga.coverUrl) + .allowHardware(false) + .tag(manga.source) + .size(coverWidth, coverHeight) + .scale(Scale.FILL) + .build(), + ).getDrawableOrThrow() + }.onSuccess { + covers[manga] = it + }.onFailure { + it.printStackTraceDebug() + }.getOrNull() + } + + private fun createChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = NotificationManagerCompat.from(context) + if (manager.getNotificationChannel(CHANNEL_ID) == null) { + val channel = NotificationChannel( + CHANNEL_ID, + context.getString(R.string.downloads), + NotificationManager.IMPORTANCE_LOW, + ) + channel.enableVibration(false) + channel.enableLights(false) + channel.setSound(null, null) + manager.createNotificationChannel(channel) + } + } + } + + @AssistedFactory + interface Factory { + + fun create(uuid: UUID): DownloadNotificationFactory + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt new file mode 100644 index 000000000..6c42b9275 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadStartedObserver.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.download.ui.worker + +import android.view.View +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 + +class DownloadStartedObserver( + private val snackbarHost: View, +) : FlowCollector { + + 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 + } + snackbar.setAction(R.string.details) { + it.context.startActivity(DownloadsActivity.newIntent(it.context)) + } + snackbar.show() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt new file mode 100644 index 000000000..0d620188d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -0,0 +1,455 @@ +package org.koitharu.kotatsu.download.ui.worker + +import android.app.NotificationManager +import android.content.Context +import android.webkit.MimeTypeMap +import androidx.core.content.ContextCompat +import androidx.hilt.work.HiltWorker +import androidx.lifecycle.asFlow +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.await +import dagger.Reusable +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.internal.closeQuietly +import okio.IOException +import okio.buffer +import okio.sink +import org.koitharu.kotatsu.R +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.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.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.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.util.ext.printStackTraceDebug +import java.io.File +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltWorker +class DownloadWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + @MangaHttpClient private val okHttp: OkHttpClient, + private val cache: PagesCache, + private val localMangaRepository: LocalMangaRepository, + private val mangaDataRepository: MangaDataRepository, + private val settings: AppSettings, + private val mangaRepositoryFactory: MangaRepository.Factory, + @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, + notificationFactoryFactory: DownloadNotificationFactory.Factory, +) : CoroutineWorker(appContext, params) { + + private val notificationFactory = notificationFactoryFactory.create(params.id) + private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + @Volatile + private var lastPublishedState: DownloadState? = null + private val currentState: DownloadState + get() = checkNotNull(lastPublishedState) + + private val pausingHandle = PausingHandle() + private val timeLeftEstimator = TimeLeftEstimator() + private val notificationThrottler = Throttler(400) + private val pausingReceiver = PausingReceiver(params.id, pausingHandle) + + override suspend fun doWork(): Result { + setForeground(getForegroundInfo()) + val mangaId = inputData.getLong(MANGA_ID, 0L) + val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure() + val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() } + val downloadedIds = getDoneChapters() + lastPublishedState = DownloadState(manga, isIndeterminate = true) + return try { + downloadMangaImpl(chaptersIds, downloadedIds) + Result.success(currentState.toWorkData()) + } catch (e: CancellationException) { + withContext(NonCancellable) { + val notification = notificationFactory.create(currentState.copy(isStopped = true)) + notificationManager.notify(id.hashCode(), notification) + } + throw e + } catch (e: IOException) { + e.printStackTraceDebug() + Result.retry() + } catch (e: Exception) { + e.printStackTraceDebug() + Result.failure( + currentState.copy( + error = e.getDisplayMessage(applicationContext.resources), + eta = -1L, + ).toWorkData(), + ) + } finally { + notificationManager.cancel(id.hashCode()) + } + } + + override suspend fun getForegroundInfo() = ForegroundInfo( + id.hashCode(), + notificationFactory.create(lastPublishedState), + ) + + private suspend fun downloadMangaImpl( + includedIds: LongArray?, + excludedIds: LongArray, + ) { + var manga = currentState.manga + val chaptersToSkip = excludedIds.toMutableSet() + withMangaLock(manga) { + ContextCompat.registerReceiver( + applicationContext, + pausingReceiver, + PausingReceiver.createIntentFilter(id), + ContextCompat.RECEIVER_NOT_EXPORTED, + ) + val destination = localMangaRepository.getOutputDir(manga) + checkNotNull(destination) { applicationContext.getString(R.string.cannot_find_available_storage) } + val tempFileName = "${manga.id}_$id.tmp" + var output: LocalMangaOutput? = null + try { + if (manga.source == MangaSource.LOCAL) { + manga = localMangaRepository.getRemoteManga(manga) + ?: error("Cannot obtain remote manga instance") + } + val repo = mangaRepositoryFactory.create(manga.source) + val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga + output = LocalMangaOutput.getOrCreate(destination, mangaDetails) + val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl } + if (coverUrl.isNotEmpty()) { + downloadFile(coverUrl, destination, tempFileName, repo.source).let { file -> + output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) + } + } + val chapters = getChapters(mangaDetails, includedIds) + for ((chapterIndex, chapter) in chapters.withIndex()) { + if (chaptersToSkip.remove(chapter.id)) { + publishState( + currentState.copy( + downloadedChapters = currentState.downloadedChapters + chapter.id, + ), + ) + continue + } + val pages = runFailsafe(pausingHandle) { + repo.getPages(chapter) + } + for ((pageIndex, page) in pages.withIndex()) { + runFailsafe(pausingHandle) { + val url = repo.getPageUrl(page) + val file = cache.get(url) + ?: downloadFile(url, destination, tempFileName, repo.source) + output.addPage( + chapter = chapter, + file = file, + pageNumber = pageIndex, + ext = MimeTypeMap.getFileExtensionFromUrl(url), + ) + } + publishState( + currentState.copy( + totalChapters = chapters.size, + currentChapter = chapterIndex, + totalPages = pages.size, + currentPage = pageIndex, + isIndeterminate = false, + eta = timeLeftEstimator.getEta(), + ), + ) + + if (settings.isDownloadsSlowdownEnabled) { + delay(SLOWDOWN_DELAY) + } + } + if (output.flushChapter(chapter)) { + runCatchingCancellable { + localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga()) + }.onFailure(Throwable::printStackTraceDebug) + } + publishState( + currentState.copy( + downloadedChapters = currentState.downloadedChapters + chapter.id, + ), + ) + } + publishState(currentState.copy(isIndeterminate = true, eta = -1L)) + output.mergeWithExisting() + output.finish() + val localManga = LocalMangaInput.of(output.rootFile).getManga() + localStorageChanges.emit(localManga) + publishState(currentState.copy(localManga = localManga, eta = -1L)) + } catch (e: Exception) { + if (e !is CancellationException) { + publishState(currentState.copy(error = e.getDisplayMessage(applicationContext.resources))) + } + throw e + } finally { + withContext(NonCancellable) { + applicationContext.unregisterReceiver(pausingReceiver) + output?.closeQuietly() + output?.cleanup() + File(destination, tempFileName).deleteAwait() + } + } + } + } + + private suspend fun runFailsafe( + pausingHandle: PausingHandle, + block: suspend () -> R, + ): R { + if (pausingHandle.isPaused) { + publishState(currentState.copy(isPaused = true, eta = -1L)) + pausingHandle.awaitResumed() + publishState(currentState.copy(isPaused = false)) + } + var countDown = MAX_FAILSAFE_ATTEMPTS + failsafe@ while (true) { + try { + return block() + } catch (e: IOException) { + if (countDown <= 0) { + publishState( + currentState.copy( + isPaused = true, + error = e.getDisplayMessage(applicationContext.resources), + eta = -1L, + ), + ) + countDown = MAX_FAILSAFE_ATTEMPTS + pausingHandle.pause() + pausingHandle.awaitResumed() + publishState(currentState.copy(isPaused = false, error = null)) + } else { + countDown-- + delay(DOWNLOAD_ERROR_DELAY) + } + } + } + } + + private suspend fun downloadFile( + url: String, + destination: File, + tempFileName: String, + source: MangaSource, + ): File { + val request = Request.Builder() + .url(url) + .tag(MangaSource::class.java, source) + .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8") + .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) + .get() + .build() + val call = okHttp.newCall(request) + val file = File(destination, tempFileName) + val response = call.clone().await() + checkNotNull(response.body).use { body -> + file.sink(append = false).buffer().use { + it.writeAllCancellable(body.source()) + } + } + return file + } + + private suspend fun publishState(state: DownloadState) { + val previousState = currentState + lastPublishedState = state + if (previousState.isParticularProgress && state.isParticularProgress) { + timeLeftEstimator.tick(state.progress, state.max) + } else { + timeLeftEstimator.emptyTick() + notificationThrottler.reset() + } + val notification = notificationFactory.create(state) + if (state.isFinalState) { + notificationManager.notify(id.toString(), id.hashCode(), notification) + } else if (notificationThrottler.throttle()) { + notificationManager.notify(id.hashCode(), notification) + } else { + return + } + setProgress(state.toWorkData()) + } + + private suspend fun getDoneChapters(): LongArray { + val work = WorkManagerHelper(WorkManager.getInstance(applicationContext)).getWorkInfoById(id) + ?: return LongArray(0) + return DownloadState.getDownloadedChapters(work.progress) + } + + private fun getChapters( + manga: Manga, + includedIds: LongArray?, + ): List { + val chapters = checkNotNull(manga.chapters?.toMutableList()) { + "Chapters list must not be null" + } + if (includedIds != null) { + val chaptersIdsSet = includedIds.toMutableSet() + chapters.retainAll { x -> chaptersIdsSet.remove(x.id) } + check(chaptersIdsSet.isEmpty()) { + "${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga" + } + } + check(chapters.isNotEmpty()) { "Chapters list must not be empty" } + return chapters + } + + private suspend inline fun withMangaLock(manga: Manga, block: () -> T) = try { + localMangaRepository.lockManga(manga.id) + block() + } finally { + localMangaRepository.unlockManga(manga.id) + } + + @Reusable + class Scheduler @Inject constructor( + @ApplicationContext private val context: Context, + private val dataRepository: MangaDataRepository, + private val settings: AppSettings, + ) { + + private val workManager: WorkManager + inline get() = WorkManager.getInstance(context) + + suspend fun schedule(manga: Manga, chaptersIds: Collection?) { + dataRepository.storeManga(manga) + val data = Data.Builder() + .putLong(MANGA_ID, manga.id) + if (!chaptersIds.isNullOrEmpty()) { + data.putLongArray(CHAPTERS_IDS, chaptersIds.toLongArray()) + } + scheduleImpl(listOf(data.build())) + } + + suspend fun schedule(manga: Collection) { + val data = manga.map { + dataRepository.storeManga(it) + Data.Builder() + .putLong(MANGA_ID, it.id) + .build() + } + scheduleImpl(data) + } + + fun observeWorks(): Flow> = workManager + .getWorkInfosByTagLiveData(TAG) + .asFlow() + + suspend fun cancel(id: UUID) { + workManager.cancelWorkById(id).await() + } + + suspend fun cancelAll() { + workManager.cancelAllWorkByTag(TAG).await() + } + + fun pause(id: UUID) { + val intent = PausingReceiver.getPauseIntent(id) + context.sendBroadcast(intent) + } + + fun resume(id: UUID) { + val intent = PausingReceiver.getResumeIntent(id) + context.sendBroadcast(intent) + } + + suspend fun delete(id: UUID) { + WorkManagerHelper(workManager).deleteWork(id) + } + + suspend fun removeCompleted() { + val helper = WorkManagerHelper(workManager) + val finishedWorks = helper.getFinishedWorkInfosByTag(TAG) + helper.deleteWorks(finishedWorks.mapToSet { it.id }) + } + + suspend fun updateConstraints() { + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .build() + val helper = WorkManagerHelper(workManager) + val works = helper.getWorkInfosByTag(TAG) + for (work in works) { + if (work.state.isFinished) { + continue + } + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setId(work.id) + .build() + helper.updateWork(request) + } + } + + private suspend fun scheduleImpl(data: Collection) { + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .build() + val requests = data.map { inputData -> + OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG) + .keepResultsForAtLeast(7, TimeUnit.DAYS) + .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) + .setInputData(inputData) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + } + workManager.enqueue(requests).await() + } + } + + private companion object { + + const val MAX_FAILSAFE_ATTEMPTS = 2 + const val DOWNLOAD_ERROR_DELAY = 500L + const val SLOWDOWN_DELAY = 100L + const val MANGA_ID = "manga_id" + const val CHAPTERS_IDS = "chapters" + const val TAG = "download" + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt similarity index 90% rename from app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt index 499f88f34..6a660c2ab 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/PausingHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.download.ui.service +package org.koitharu.kotatsu.download.ui.worker import androidx.annotation.AnyThread import kotlinx.coroutines.flow.MutableStateFlow @@ -27,4 +27,4 @@ class PausingHandle { fun resume() { paused.value = false } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt new file mode 100644 index 000000000..71dc7aa4c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingReceiver.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.download.ui.worker + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.PatternMatcher +import androidx.core.app.PendingIntentCompat +import org.koitharu.kotatsu.core.util.ext.toUUIDOrNull +import java.util.UUID + +class PausingReceiver( + private val id: UUID, + private val pausingHandle: PausingHandle, +) : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + val uuid = intent?.getStringExtra(EXTRA_UUID)?.toUUIDOrNull() + if (uuid != id) { + return + } + when (intent.action) { + ACTION_RESUME -> pausingHandle.resume() + ACTION_PAUSE -> pausingHandle.pause() + } + } + + companion object { + + private const val ACTION_PAUSE = "org.koitharu.kotatsu.download.PAUSE" + private const val ACTION_RESUME = "org.koitharu.kotatsu.download.RESUME" + private const val EXTRA_UUID = "uuid" + private const val SCHEME = "workuid" + + fun createIntentFilter(id: UUID) = IntentFilter().apply { + addAction(ACTION_PAUSE) + addAction(ACTION_RESUME) + addDataScheme(SCHEME) + addDataPath(id.toString(), PatternMatcher.PATTERN_SIMPLE_GLOB) + } + + fun getPauseIntent(id: UUID) = Intent(ACTION_PAUSE) + .setData(Uri.parse("$SCHEME://$id")) + .putExtra(EXTRA_UUID, id.toString()) + + fun getResumeIntent(id: UUID) = Intent(ACTION_RESUME) + .setData(Uri.parse("$SCHEME://$id")) + .putExtra(EXTRA_UUID, id.toString()) + + fun createPausePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast( + context, + 0, + getPauseIntent(id), + 0, + false, + ) + + fun createResumePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast( + context, + 0, + getResumeIntent(id), + 0, + false, + ) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt new file mode 100644 index 000000000..de68c4097 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.explore.domain + +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.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.util.ext.printStackTraceDebug +import javax.inject.Inject + +class ExploreRepository @Inject constructor( + private val settings: AppSettings, + private val historyRepository: HistoryRepository, + private val mangaRepositoryFactory: MangaRepository.Factory, +) { + + suspend fun findRandomManga(tagsLimit: Int): Manga { + val blacklistTagRegex = TagsBlacklist(settings.suggestionsTagsBlacklist, 0.4f) + val tags = historyRepository.getPopularTags(tagsLimit).mapNotNull { + if (it in blacklistTagRegex) null else it.title + } + val sources = settings.getMangaSources(includeHidden = false) + check(sources.isNotEmpty()) { "No sources available" } + for (i in 0..4) { + val list = getList(sources.random(), tags, blacklistTagRegex) + val manga = list.randomOrNull() ?: continue + val details = runCatchingCancellable { + mangaRepositoryFactory.create(manga.source).getDetails(manga) + }.getOrNull() ?: continue + if ((settings.isSuggestionsExcludeNsfw && details.isNsfw) || details in blacklistTagRegex) { + continue + } + return details + } + throw NoSuchElementException() + } + + private suspend fun getList( + source: MangaSource, + tags: List, + blacklist: TagsBlacklist, + ): List = runCatchingCancellable { + val repository = mangaRepositoryFactory.create(source) + val order = repository.sortOrders.random() + val availableTags = repository.getTags() + val tag = tags.firstNotNullOfOrNull { title -> + availableTags.find { x -> x.title.almostEquals(title, 0.4f) } + } + val list = repository.getList(0, setOfNotNull(tag), order).asArrayList() + if (settings.isSuggestionsExcludeNsfw) { + list.removeAll { it.isNsfw } + } + if (blacklist.isNotEmpty()) { + list.removeAll { manga -> manga in blacklist } + } + list.shuffle() + list + }.onFailure { + it.printStackTraceDebug() + }.getOrDefault(emptyList()) +} 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 71% 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 7d8e326b8..69b6c9d5c 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 @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.explore.ui +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem @@ -15,13 +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.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 @@ -34,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 @@ -52,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 @@ -68,14 +72,17 @@ 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) { + showSuggestionsTip() + } } override fun onDestroyView() { @@ -84,7 +91,7 @@ class ExploreFragment : } override fun onWindowInsetsChanged(insets: Insets) { - binding.recyclerView.updatePadding( + requireViewBinding().recyclerView.updatePadding( bottom = insets.bottom, ) } @@ -133,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) } @@ -143,6 +150,19 @@ class ExploreFragment : activity?.invalidateOptionsMenu() } + private fun showSuggestionsTip() { + val listener = DialogInterface.OnClickListener { _, which -> + viewModel.respondSuggestionTip(which == DialogInterface.BUTTON_POSITIVE) + } + TwoButtonsAlertDialog.Builder(requireContext()) + .setIcon(R.drawable.ic_suggestion) + .setTitle(R.string.suggestions_enable_prompt) + .setPositiveButton(R.string.enable, listener) + .setNegativeButton(R.string.no_thanks, listener) + .create() + .show() + } + private inner class SourceMenuListener( private val sourceItem: ExploreItem.Source, ) : PopupMenu.OnMenuItemClickListener { 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 66% 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 4b87979f7..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,60 +1,70 @@ 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.distinctUntilChanged 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" + @HiltViewModel class ExploreViewModel @Inject constructor( private val settings: AppSettings, 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 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.call(Unit) + } + } + } fun openRandom() { launchLoadingJob(Dispatchers.Default) { val manga = exploreRepository.findRandomManga(tagsLimit = 8) - onOpenManga.emitCall(manga) + onOpenManga.call(manga) } } @@ -64,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)) } } @@ -72,6 +82,11 @@ class ExploreViewModel @Inject constructor( settings.isSourcesGridMode = value } + fun respondSuggestionTip(isAccepted: Boolean) { + settings.isSuggestionsEnabled = isAccepted + settings.closeTip(TIP_SUGGESTIONS) + } + private fun createContentFlow() = settings.observe() .filter { it == AppSettings.KEY_SOURCES_HIDDEN || @@ -80,8 +95,7 @@ class ExploreViewModel @Inject constructor( } .onStart { emit("") } .map { settings.getMangaSources(includeHidden = false) } - .distinctUntilChanged() - .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 100% 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 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 65% 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 index 36308d4c6..af468c3e8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/EntityMapping.kt @@ -1,9 +1,11 @@ package org.koitharu.kotatsu.favourites.data import org.koitharu.kotatsu.core.db.entity.SortOrder +import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.parsers.model.SortOrder -import java.util.* +import java.util.Date fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) = FavouriteCategory( id = id, @@ -13,4 +15,8 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) createdAt = Date(createdAt), isTrackingEnabled = track, isVisibleInLibrary = isVisibleInLibrary, -) \ No newline at end of file +) + +fun FavouriteManga.toManga() = manga.toManga(tags.toMangaTags()) + +fun Collection.toMangaList() = map { it.toManga() } 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 96% 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 index 0e0ee05de..590b66436 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -17,6 +17,10 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC") abstract suspend fun findAll(): List + @Transaction + @Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit") + abstract suspend fun findLast(limit: Int): List + fun observeAll(order: SortOrder): Flow> { val orderBy = getOrderBy(order) 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 92% 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 3875eb2e5..e99653964 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,21 +7,21 @@ 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.db.entity.toManga -import org.koitharu.kotatsu.core.db.entity.toMangaTags 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 +import org.koitharu.kotatsu.favourites.data.toManga +import org.koitharu.kotatsu.favourites.data.toMangaList 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 @@ -32,22 +32,27 @@ class FavouritesRepository @Inject constructor( suspend fun getAllManga(): List { val entities = db.favouritesDao.findAll() - return entities.map { it.manga.toManga(it.tags.toMangaTags()) } + return entities.toMangaList() + } + + suspend fun getLastManga(limit: Int): List { + val entities = db.favouritesDao.findLast(limit) + return entities.toMangaList() } fun observeAll(order: SortOrder): Flow> { return db.favouritesDao.observeAll(order) - .mapItems { it.manga.toManga(it.tags.toMangaTags()) } + .mapItems { it.toManga() } } suspend fun getManga(categoryId: Long): List { val entities = db.favouritesDao.findAll(categoryId) - return entities.map { it.manga.toManga(it.tags.toMangaTags()) } + return entities.toMangaList() } fun observeAll(categoryId: Long, order: SortOrder): Flow> { return db.favouritesDao.observeAll(categoryId, order) - .mapItems { it.manga.toManga(it.tags.toMangaTags()) } + .mapItems { it.toManga() } } fun observeAll(categoryId: Long): Flow> { 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..2675851ae 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) } @@ -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 92% 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 d69ea5ec1..57d73ad5e 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,14 +15,14 @@ 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.animatorDurationScale +import org.koitharu.kotatsu.core.util.ext.disposeImageRequest +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.newImageRequest 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.animatorDurationScale -import org.koitharu.kotatsu.utils.ext.disposeImageRequest -import org.koitharu.kotatsu.utils.ext.enqueueWith -import org.koitharu.kotatsu.utils.ext.getThemeColor -import org.koitharu.kotatsu.utils.ext.newImageRequest 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 8549bd0a7..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,15 +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 com.google.android.material.R as materialR @AndroidEntryPoint @@ -45,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 } } @@ -74,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 } } @@ -108,42 +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.isChecked = category?.isTrackingEnabled ?: true - binding.switchTracker.jumpDrawablesToCurrentState() - binding.switchShelf.isChecked = category?.isVisibleInLibrary ?: true - binding.switchShelf.jumpDrawablesToCurrentState() + 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/FavouriteCategoriesBottomSheet.kt similarity index 82% 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/FavouriteCategoriesBottomSheet.kt index 93bf67e1d..983246319 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/FavouriteCategoriesBottomSheet.kt @@ -11,16 +11,18 @@ 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.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +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.parsers.model.Manga -import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.withArgs @AndroidEntryPoint class FavouriteCategoriesBottomSheet : @@ -33,20 +35,20 @@ class FavouriteCategoriesBottomSheet : 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() { 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 88% 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..69180f1aa 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,16 @@ 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.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.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem -import org.koitharu.kotatsu.utils.asFlowLiveData import javax.inject.Inject @HiltViewModel @@ -33,7 +35,7 @@ class MangaCategoriesViewModel @Inject constructor( 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/java/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 similarity index 94% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoriesAdapter.kt index df6e54ca0..351037748 100644 --- a/app/src/main/java/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 @@ -2,7 +2,7 @@ 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.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem class MangaCategoriesAdapter( @@ -34,4 +34,4 @@ class MangaCategoriesAdapter( return super.getChangePayload(oldItem, newItem) } } -} \ No newline at end of file +} 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 89% 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..3badc482b 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,14 @@ 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 fun mangaCategoryAD( clickListener: OnListItemClickListener ) = adapterDelegateViewBinding( - { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }, ) { itemView.setOnClickListener { @@ -21,4 +21,4 @@ fun mangaCategoryAD( isChecked = item.isChecked } } -} \ No newline at end of file +} 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 100% 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 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 76% 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 d540d1c17..3c7ceb019 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,23 +1,28 @@ 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.history.data.HistoryRepository +import org.koitharu.kotatsu.history.data.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,7 +31,6 @@ 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 @@ -37,16 +41,17 @@ class FavouritesListViewModel @Inject constructor( private val historyRepository: HistoryRepository, private val settings: AppSettings, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings), ListExtraProvider { + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { 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( @@ -55,7 +60,7 @@ class FavouritesListViewModel @Inject constructor( } else { repository.observeAll(categoryId) }, - listModeFlow, + listMode, ) { list, mode -> when { list.isEmpty() -> listOf( @@ -75,7 +80,7 @@ class FavouritesListViewModel @Inject constructor( } }.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 +96,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)) } } 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 100% 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 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..f8977fabf 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 @@ -161,18 +157,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..a28d27fb8 --- /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.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 75% 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 ef8f0f720..c46f349db 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,23 +1,28 @@ 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.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.MangaWithHistory -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +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.data.HistoryRepository +import org.koitharu.kotatsu.history.data.PROGRESS_NONE +import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -27,10 +32,6 @@ 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 @@ -41,17 +42,19 @@ class HistoryListViewModel @Inject constructor( private val settings: AppSettings, private val trackingRepository: TrackingRepository, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings) { + 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( @@ -71,7 +74,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 @@ -89,7 +92,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)) } } 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/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt 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 79% 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..2b7141385 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,12 +9,12 @@ 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.BaseBottomSheet +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 @@ -27,13 +26,13 @@ class ListModeBottomSheet : @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 77% 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 0cbb8cff7..146767467 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,21 +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.getThemeColor +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.service.DownloadService +import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID @@ -48,14 +58,6 @@ 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.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 +90,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, ) @@ -123,8 +125,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.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) + viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) } override fun onDestroyView() { @@ -162,7 +165,7 @@ abstract class MangaListFragment : @CallSuper override fun onRefresh() { - binding.swipeRefreshLayout.isRefreshing = true + requireViewBinding().swipeRefreshLayout.isRefreshing = true viewModel.onRefresh() } @@ -184,10 +187,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 } } @@ -200,15 +203,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), @@ -232,12 +235,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) { @@ -268,7 +271,7 @@ abstract class MangaListFragment : addOnLayoutChangeListener(spanResolver) } } - selectionController?.attachToRecyclerView(binding.recyclerView) + selectionController?.attachToRecyclerView(requireViewBinding().recyclerView) } } @@ -299,7 +302,7 @@ abstract class MangaListFragment : } R.id.action_save -> { - DownloadService.confirmAndStart(binding.recyclerView, selectedItems) + viewModel.download(selectedItems) mode.finish() true } @@ -309,16 +312,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 { @@ -342,7 +345,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/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt new file mode 100644 index 000000000..8c197fae9 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.list.ui + +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.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.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 + +abstract class MangaListViewModel( + settings: AppSettings, + private val downloadScheduler: DownloadWorker.Scheduler, +) : BaseViewModel() { + + 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 = MutableEventFlow() + + open fun onUpdateFilter(tags: Set) = Unit + + abstract fun onRefresh() + + abstract fun onRetry() + + fun download(items: Set) { + launchJob(Dispatchers.Default) { + downloadScheduler.schedule(items) + 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 71% 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 index 2ec86a91d..2cb70f71a 100644 --- 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 @@ -4,11 +4,13 @@ package org.koitharu.kotatsu.list.ui.adapter import android.view.View import androidx.annotation.CheckResult +import androidx.cardview.widget.CardView import androidx.core.view.doOnNextLayout import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils import org.koitharu.kotatsu.R +import com.google.android.material.R as materialR @CheckResult fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { @@ -16,7 +18,7 @@ fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { val badgeDrawable = badge ?: initBadge(this) badgeDrawable.number = counter badgeDrawable.isVisible = true - badgeDrawable.align() + badgeDrawable.align(this) badgeDrawable } else { badge?.isVisible = false @@ -34,12 +36,17 @@ private fun initBadge(anchor: View): BadgeDrawable { badge.maxCharacterCount = resources.getInteger(R.integer.manga_badge_max_character_count) anchor.doOnNextLayout { BadgeUtils.attachBadgeDrawable(badge, it) - badge.align() + badge.align(it) } return badge } -private fun BadgeDrawable.align() { - horizontalOffset = intrinsicWidth - verticalOffset = intrinsicHeight +private fun BadgeDrawable.align(anchor: View) { + val extraOffset = if (anchor is CardView) { + (anchor.radius / 2f).toInt() + } else { + anchor.resources.getDimensionPixelOffset(materialR.dimen.m3_badge_offset) + } + horizontalOffset = intrinsicWidth + extraOffset + verticalOffset = intrinsicHeight + extraOffset } 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 87% 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..4a0f8f2fe 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,13 +1,13 @@ package org.koitharu.kotatsu.list.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.core.ui.model.titleRes +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.databinding.ItemHeader2Binding import org.koitharu.kotatsu.list.ui.model.ListHeader2 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 fun listHeader2AD( listener: MangaListListener, 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 74% 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..51a58e2e0 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,20 @@ 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.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 { 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 94% 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..f471cc99a 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.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 } 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/filter/FilterAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt similarity index 92% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt index e34b5ad94..5dcb3a21a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterAdapterDelegates.kt @@ -5,14 +5,14 @@ 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.core.ui.model.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) } + { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }, ) { itemView.setOnClickListener { @@ -28,7 +28,7 @@ fun filterSortDelegate( fun filterTagDelegate( listener: OnFilterChangedListener, ) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) } + { layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }, ) { itemView.setOnClickListener { @@ -42,7 +42,7 @@ fun filterTagDelegate( } fun filterHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } + { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }, ) { bind { @@ -63,4 +63,4 @@ fun filterErrorDelegate() = adapterDelegate(R.layo 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/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt index 226d73ed6..a00341a55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -3,18 +3,18 @@ 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.core.ui.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.parentFragmentViewModels import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel -import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels class FilterBottomSheet : BaseBottomSheet(), @@ -25,12 +25,12 @@ class FilterBottomSheet : private val viewModel by parentFragmentViewModels() private var collapsibleActionViewCallback: CollapseActionViewCallback? = null - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { return SheetFilterBinding.inflate(inflater, container, false) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) val adapter = FilterAdapter(viewModel, this) binding.recyclerView.adapter = adapter viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems) @@ -65,13 +65,13 @@ class FilterBottomSheet : override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { if (currentList.size > previousList.size && view != null) { - (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0) + (requireViewBinding().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) + requireViewBinding().headerBar.inflateMenu(R.menu.opt_filter) + val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search) searchMenuItem.setOnActionExpandListener(this) val searchView = searchMenuItem.actionView as SearchView searchView.setOnQueryTextListener(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index 96fb630ae..0ff57d7a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -1,24 +1,27 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.annotation.WorkerThread -import androidx.lifecycle.LiveData import kotlinx.coroutines.CoroutineScope 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.MangaDataRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository 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.util.ext.printStackTraceDebug import java.text.Collator import java.util.Locale import java.util.TreeSet @@ -31,13 +34,13 @@ class FilterCoordinator( 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)) + val items: StateFlow> = getItemsFlow() + .stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading)) init { observeState() @@ -97,7 +100,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) { @@ -188,9 +191,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/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterDiffCallback.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterItem.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterState.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterState.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/FilterState.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/filter/OnFilterChangedListener.kt 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/ListHeader2.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt index 0f053bfd6..dab335ee2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader2.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader2.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.SortOrder class ListHeader2( @@ -16,10 +16,9 @@ class ListHeader2( other as ListHeader2 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 +26,4 @@ class ListHeader2( result = 31 * result + (sortOrder?.hashCode() ?: 0) return result } -} \ No newline at end of file +} 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 95% 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..ea24b01fa 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,16 +1,16 @@ 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 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 87% 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..7ac0dc282 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,25 +11,22 @@ 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.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.util.ext.printStackTraceDebug import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -65,7 +62,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 +97,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) } 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 66% 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 1de0e9516..755ed222b 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 @@ -6,17 +6,19 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible 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.copyToSuspending -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.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.util.ext.printStackTraceDebug import java.io.File -import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton @@ -49,11 +51,11 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { } } - suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) { + suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) { val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) try { - file.outputStream().use { out -> - inputStream.copyToSuspending(out) + file.sink(append = false).buffer().use { + it.writeAllCancellable(source) } lruCache.get().put(url, file) } finally { 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 76% 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 7b170eb19..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 @@ -7,16 +7,19 @@ import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +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.copyToSuspending -import org.koitharu.kotatsu.utils.ext.resolveName +import org.koitharu.kotatsu.local.domain.model.LocalManga import java.io.File import java.io.IOException import javax.inject.Inject @@ -30,17 +33,17 @@ class SingleMangaImporter @Inject constructor( private val contentResolver = context.contentResolver - suspend fun import(uri: Uri, progressState: MutableStateFlow?): LocalManga { + suspend fun import(uri: Uri): LocalManga { val result = if (isDirectory(uri)) { - importDirectory(uri, progressState) + importDirectory(uri) } else { - importFile(uri, progressState) + importFile(uri) } localStorageChanges.emit(result) return result } - private suspend fun importFile(uri: Uri, progressState: MutableStateFlow?): LocalManga { + private suspend fun importFile(uri: Uri): LocalManga = withContext(Dispatchers.IO) { val contentResolver = storageManager.contentResolver val name = contentResolver.resolveName(uri) ?: throw IOException("Cannot fetch name from uri: $uri") if (!CbzFilter.isFileSupported(name)) { @@ -50,14 +53,14 @@ class SingleMangaImporter @Inject constructor( runInterruptible { contentResolver.openInputStream(uri) }?.use { source -> - dest.outputStream().use { output -> - source.copyToSuspending(output, progressState = progressState) + dest.sink().buffer().use { output -> + output.writeAllCancellable(source.source()) } } ?: throw IOException("Cannot open input stream: $uri") - return LocalMangaInput.of(dest).getManga() + LocalMangaInput.of(dest).getManga() } - private suspend fun importDirectory(uri: Uri, progressState: MutableStateFlow?): LocalManga { + private suspend fun importDirectory(uri: Uri): LocalManga { val root = requireNotNull(DocumentFile.fromTreeUri(context, uri)) { "Provided uri $uri is not a tree" } @@ -80,9 +83,9 @@ class SingleMangaImporter @Inject constructor( docFile.copyTo(subDir) } } else { - inputStream().use { input -> - File(destDir, requireName()).outputStream().use { output -> - input.copyToSuspending(output) + inputStream().source().use { input -> + File(destDir, requireName()).sink().buffer().use { output -> + output.writeAllCancellable(input) } } } 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..e62943d07 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() 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 90% 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 64f076741..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 @@ -31,7 +31,8 @@ sealed class LocalMangaInput( } @JvmStatic - protected fun zipUri(file: File, entryName: String) = "cbz://${file.path}#$entryName" + protected fun zipUri(file: File, entryName: String): String = + Uri.fromParts("cbz", file.path, entryName).toString() @JvmStatic protected fun Manga.copy2( 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/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt new file mode 100644 index 000000000..30c161037 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt @@ -0,0 +1,93 @@ +package org.koitharu.kotatsu.local.data.output + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import okio.Closeable +import org.koitharu.kotatsu.local.data.input.LocalMangaInput +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.toFileNameSafe +import java.io.File + +sealed class LocalMangaOutput( + val rootFile: File, +) : Closeable { + + abstract suspend fun mergeWithExisting() + + abstract suspend fun addCover(file: File, ext: String) + + abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) + + abstract suspend fun flushChapter(chapter: MangaChapter): Boolean + + abstract suspend fun finish() + + abstract suspend fun cleanup() + + companion object { + + const val ENTRY_NAME_INDEX = "index.json" + const val SUFFIX_TMP = ".tmp" + private val mutex = Mutex() + + suspend fun getOrCreate(root: File, manga: Manga): LocalMangaOutput = withContext(Dispatchers.IO) { + val preferSingleCbz = manga.chapters.let { + it != null && it.size <= 3 + } + checkNotNull(getImpl(root, manga, onlyIfExists = false, preferSingleCbz)) + } + + suspend fun get(root: File, manga: Manga): LocalMangaOutput? = withContext(Dispatchers.IO) { + getImpl(root, manga, onlyIfExists = true, preferSingleCbz = false) + } + + private suspend fun getImpl( + root: File, + manga: Manga, + onlyIfExists: Boolean, + preferSingleCbz: Boolean, + ): LocalMangaOutput? { + mutex.withLock { + var i = 0 + val baseName = manga.title.toFileNameSafe() + while (true) { + val fileName = if (i == 0) baseName else baseName + "_$i" + val dir = File(root, fileName) + val zip = File(root, "$fileName.cbz") + i++ + return when { + dir.isDirectory -> { + if (canWriteTo(dir, manga)) { + LocalMangaDirOutput(dir, manga) + } else { + continue + } + } + + zip.isFile -> if (canWriteTo(zip, manga)) { + LocalMangaZipOutput(zip, manga) + } else { + continue + } + + !onlyIfExists -> if (preferSingleCbz) { + LocalMangaZipOutput(zip, manga) + } else { + LocalMangaDirOutput(dir, manga) + } + + else -> null + } + } + } + } + + private suspend fun canWriteTo(file: File, manga: Manga): Boolean { + val info = LocalMangaInput.of(file).getMangaInfo() ?: return false + return info.id == manga.id + } + } +} 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..ef613604f --- /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.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 88% 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..6ff59f3a3 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,7 +10,7 @@ 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 @@ -27,7 +27,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 +38,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) 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 92% 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 1d008d6b0..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 @@ -9,7 +9,6 @@ import android.net.Uri import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat -import androidx.core.content.ContextCompat import androidx.hilt.work.HiltWorker import androidx.work.Constraints import androidx.work.CoroutineWorker @@ -24,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( @@ -48,7 +47,7 @@ class ImportWorker @AssistedInject constructor( val uri = inputData.getString(DATA_URI)?.toUriOrNull() ?: return Result.failure() setForeground(getForegroundInfo()) val result = runCatchingCancellable { - importer.import(uri, null).manga + importer.import(uri).manga } val notification = buildNotification(result) notificationManager.notify(uri.hashCode(), notification) @@ -70,12 +69,12 @@ class ImportWorker @AssistedInject constructor( .setContentTitle(title) .setPriority(NotificationCompat.PRIORITY_MIN) .setDefaults(0) - .setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)) .setSilent(true) + .setOngoing(true) .setProgress(0, 0, true) .setSmallIcon(android.R.drawable.stat_sys_download) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) - .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) .build() return ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification) @@ -85,7 +84,6 @@ class ImportWorker @AssistedInject constructor( val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setDefaults(0) - .setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)) .setSilent(true) result.onSuccess { manga -> notification.setLargeIcon( 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 89% 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 ae742f144..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 @@ -56,7 +56,6 @@ class LocalChaptersRemoveService : CoroutineIntentService() { .setContentTitle(getString(R.string.error_occurred)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setDefaults(0) - .setColor(ContextCompat.getColor(this, R.color.blue_primary_dark)) .setSilent(true) .setContentText(error.getDisplayMessage(resources)) .setSmallIcon(android.R.drawable.stat_notify_error) @@ -82,7 +81,6 @@ class LocalChaptersRemoveService : CoroutineIntentService() { .setContentTitle(title) .setPriority(NotificationCompat.PRIORITY_MIN) .setDefaults(0) - .setColor(ContextCompat.getColor(this, R.color.blue_primary_dark)) .setSilent(true) .setProgress(0, 0, true) .setSmallIcon(android.R.drawable.stat_notify_sync) 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 78% 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..3f8fd2bb2 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 @@ -12,20 +12,22 @@ 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.databinding.FragmentListBinding import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.utils.ShareHelper -import org.koitharu.kotatsu.utils.ext.addMenuProvider class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener { 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() { @@ -34,7 +36,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener override fun onFilterClick(view: View?) { super.onFilterClick(view) - val menu = PopupMenu(requireContext(), view ?: binding.recyclerView) + val menu = PopupMenu(requireContext(), view ?: requireViewBinding().recyclerView) menu.inflate(R.menu.popup_order) menu.setOnMenuItemClickListener(this) menu.show() @@ -53,12 +55,14 @@ 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) } } @@ -87,7 +91,7 @@ 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 { 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/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt similarity index 77% rename from app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 575a566ec..80836cdd7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -1,7 +1,5 @@ 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 @@ -10,15 +8,20 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus 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.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +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.history.data.HistoryRepository +import org.koitharu.kotatsu.history.data.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,17 +29,14 @@ 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.LocalMangaRepository 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.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 @@ -48,10 +48,12 @@ class LocalListViewModel @Inject constructor( private val settings: AppSettings, private val tagHighlighter: MangaTagHighlighter, @LocalStorageChanges private val localStorageChanges: SharedFlow, -) : MangaListViewModel(settings), ListExtraProvider { + private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase, + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { - val onMangaRemoved = SingleLiveEvent() - val sortOrder = MutableLiveData(settings.localListOrder) + val onMangaRemoved = MutableEventFlow() + val sortOrder = MutableStateFlow(settings.localListOrder) private val listError = MutableStateFlow(null) private val mangaList = MutableStateFlow?>(null) private val selectedTags = MutableStateFlow>(emptySet()) @@ -59,8 +61,8 @@ class LocalListViewModel @Inject constructor( override val content = combine( mangaList, - listModeFlow, - sortOrder.asFlow(), + listMode, + sortOrder, selectedTags, listError, ) { list, mode, order, tags, error -> @@ -81,7 +83,7 @@ class LocalListViewModel @Inject constructor( list.toUi(this, mode, this@LocalListViewModel, tagHighlighter) } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) init { onRefresh() @@ -118,18 +120,8 @@ class LocalListViewModel @Inject constructor( 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) + deleteLocalMangaUseCase(ids) + onMangaRemoved.call(Unit) } } 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 82% 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..024cb6994 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 @@ -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,42 @@ 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(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager) navigationDelegate.addOnFragmentChangedListener(this) navigationDelegate.onCreate(savedInstanceState) - onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container)) + onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container)) onBackPressedDispatcher.addCallback(navigationDelegate) onBackPressedDispatcher.addCallback(closeSearchCallback) @@ -136,8 +139,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 +156,13 @@ class MainActivity : override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) { adjustFabVisibility(topFragment = fragment) if (fromUser) { - binding.appbar.setExpanded(true) + 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 +176,7 @@ class MainActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.root.updatePadding( + viewBinding.root.updatePadding( left = insets.left, right = insets.right, ) @@ -198,7 +201,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 +219,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) { @@ -250,7 +253,7 @@ class MainActivity : } 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() } @@ -270,17 +273,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,7 +296,7 @@ class MainActivity : } private fun onSearchClosed() { - binding.searchView.hideKeyboard() + viewBinding.searchView.hideKeyboard() adjustSearchUI(isOpened = false, animate = true) closeSearchCallback.isEnabled = false } @@ -306,7 +309,7 @@ class MainActivity : hide() } } - binding.navRail?.isVisible = visible + viewBinding.navRail?.isVisible = visible } private fun isSearchOpened(): Boolean { @@ -341,7 +344,7 @@ class MainActivity : topFragment: Fragment? = navigationDelegate.primaryFragment, isSearchOpened: Boolean = isSearchOpened(), ) { - val fab = binding.fab ?: return + val fab = viewBinding.fab ?: return if ( isResumeEnabled && !actionModeDelegate.isActionModeStarted && @@ -360,22 +363,22 @@ 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, @@ -396,7 +399,7 @@ class MainActivity : override fun onActivityResult(result: String?) { if (result != null) { - binding.searchView.query = result + viewBinding.searchView.query = result } } } @@ -405,7 +408,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 95% 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..fe23e3cc2 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 @@ -10,13 +10,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" 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/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt similarity index 65% rename from app/src/main/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt index 78c4f29a2..e03f90ad2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/owners/NoModalBottomSheetOwner.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.main.ui.owners -import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar +import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar interface NoModalBottomSheetOwner { 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..fe4ad571f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -0,0 +1,97 @@ +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.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.await +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.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, +) { + + 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) + okHttpClient.newCall(request).await().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 83% 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 c1403fd0d..49701829a 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 @@ -20,19 +20,22 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient import okhttp3.Request +import okio.source import org.koitharu.kotatsu.core.network.CommonHeaders +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.RetainedLifecycleCoroutineScope +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.RetainedLifecycleCoroutineScope -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.withProgress -import org.koitharu.kotatsu.utils.progress.ProgressDeferred +import org.koitharu.kotatsu.util.ext.printStackTraceDebug import java.io.File import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger @@ -41,13 +44,10 @@ 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 - @ActivityRetainedScoped class PageLoader @Inject constructor( lifecycle: ActivityRetainedLifecycle, - private val okHttp: OkHttpClient, + @MangaHttpClient private val okHttp: OkHttpClient, private val cache: PagesCache, private val settings: AppSettings, private val mangaRepositoryFactory: MangaRepository.Factory, @@ -74,7 +74,7 @@ class PageLoader @Inject constructor( } fun isPrefetchApplicable(): Boolean { - return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() + return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled } @AnyThread @@ -178,7 +178,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 -> @@ -186,26 +186,20 @@ class PageLoader @Inject constructor( val entry = zip.getEntry(uri.fragment) zip.getInputStream(entry) }.use { - cache.put(pageUrl, it) + cache.put(pageUrl, it.source()) } } } 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() + val request = createPageRequest(page, pageUrl) okHttp.newCall(request).await().use { response -> check(response.isSuccessful) { - "Invalid response: ${response.code} ${response.message}" + "Invalid response: ${response.code} ${response.message} at $pageUrl" } val body = checkNotNull(response.body) { "Null response" } - body.withProgress(progress).byteStream().use { - cache.put(pageUrl, it) + body.withProgress(progress).use { + cache.put(pageUrl, it.source()) } } } @@ -217,6 +211,19 @@ class PageLoader @Inject constructor( override fun handleException(context: CoroutineContext, exception: Throwable) { exception.printStackTraceDebug() } + } + companion object { + + private const val PROGRESS_UNDEFINED = -1f + private const val PREFETCH_LIMIT_DEFAULT = 10 + + 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 100% 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt similarity index 82% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt index 87b92acd5..568a79e1e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ChaptersBottomSheet.kt @@ -7,18 +7,18 @@ 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.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +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 @@ -28,12 +28,12 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC @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,7 +46,6 @@ class ChaptersBottomSheet : BaseBottomSheet(), OnListItemC isCurrent = index == currentPosition, isUnread = index > currentPosition, isNew = false, - isMissing = false, isDownloaded = false, ) } 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 79% 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 4fdeeab85..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 @@ -12,11 +13,13 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrl import okio.IOException -import org.koitharu.kotatsu.base.domain.MangaDataRepository +import okio.buffer +import okio.sink +import okio.source +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.copyToSuspending import java.io.File import javax.inject.Inject import kotlin.coroutines.Continuation @@ -49,10 +52,10 @@ class PageSaveHelper @Inject constructor( } } runInterruptible(Dispatchers.IO) { - contentResolver.openOutputStream(destination) + contentResolver.openOutputStream(destination)?.sink()?.buffer() }?.use { output -> - pageFile.inputStream().use { input -> - input.copyToSuspending(output) + pageFile.source().use { input -> + output.writeAllCancellable(input) } } ?: throw IOException("Output stream is null") return destination @@ -71,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 { @@ -80,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 71% 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 1138ad144..f3f655ede 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 @@ -30,29 +30,32 @@ 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.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.observeWithPrevious -import org.koitharu.kotatsu.utils.ext.postDelayed -import org.koitharu.kotatsu.utils.ext.setValueRounded import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -101,40 +104,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() } } @@ -149,13 +152,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)) } + viewBinding.slider.isRtl = mode == ReaderMode.REVERSED } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -178,21 +185,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() @@ -211,30 +214,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 { @@ -257,17 +260,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) } } } @@ -279,14 +284,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() } } @@ -300,18 +305,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 { @@ -323,12 +328,12 @@ 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( + viewBinding.appbarBottom?.updatePadding( bottom = systemBars.bottom, right = systemBars.right, left = systemBars.left, @@ -349,7 +354,7 @@ class ReaderActivity : } override fun toggleUiVisibility() { - setUiIsVisible(!binding.appbarTop.isVisible) + setUiIsVisible(!viewBinding.appbarTop.isVisible) } override fun isReaderResumed(): Boolean { @@ -358,21 +363,22 @@ 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?) { + private fun onUiStateChanged(pair: Pair) { + val (uiState: ReaderUiState?, previous: ReaderUiState?) = pair title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_) - binding.infoBar.update(uiState) + 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) { @@ -382,15 +388,15 @@ class ReaderActivity : } 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 } } @@ -407,10 +413,11 @@ class ReaderActivity : .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) } - fun newIntent(context: Context, manga: Manga, branch: String?): Intent { + 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 newIntent(context: Context, manga: Manga, state: ReaderState?): Intent { 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 95% 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 02e7f91ad..7c2eb733a 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 @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.reader.ui +import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -16,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.util.ext.printStackTraceDebug import java.text.SimpleDateFormat import java.util.Date import com.google.android.material.R as materialR @@ -179,6 +180,7 @@ class ReaderInfoBarView @JvmOverloads constructor( } } + @SuppressLint("DiscouragedApi") private fun getSystemUiDimensionOffset(name: String, fallback: Int = 0): Int = runCatching { val manager = context.packageManager val resources = manager.getResourcesForApplication("com.android.systemui") 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 82% 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 index 5bf7eee5b..e454a7106 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -4,7 +4,7 @@ import androidx.annotation.IdRes import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import org.koitharu.kotatsu.core.prefs.ReaderMode -import org.koitharu.kotatsu.reader.ui.pager.BaseReader +import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment @@ -15,7 +15,7 @@ class ReaderManager( @IdRes private val containerResId: Int, ) { - private val modeMap = EnumMap>>(ReaderMode::class.java) + private val modeMap = EnumMap>>(ReaderMode::class.java) init { modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java @@ -23,8 +23,8 @@ class ReaderManager( modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java } - val currentReader: BaseReader<*>? - get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*> + val currentReader: BaseReaderFragment<*>? + get() = fragmentManager.findFragmentById(containerResId) as? BaseReaderFragment<*> val currentMode: ReaderMode? get() { @@ -40,7 +40,7 @@ class ReaderManager( } } - fun replace(reader: BaseReader<*>) { + fun replace(reader: BaseReaderFragment<*>) { fragmentManager.commit { setReorderingAllowed(true) replace(containerResId, reader) 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 63% 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 c877742b9..f32dbbbb6 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,9 +13,12 @@ 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 +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -26,38 +26,36 @@ 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.parser.MangaRepository +import org.koitharu.kotatsu.core.os.ShortcutsUpdater +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.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 org.koitharu.kotatsu.util.ext.printStackTraceDebug import java.util.Date import javax.inject.Inject @@ -67,7 +65,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, @@ -75,6 +72,10 @@ class ReaderViewModel @Inject constructor( private val pageSaveHelper: PageSaveHelper, 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) @@ -86,33 +87,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 }, ) @@ -120,28 +121,28 @@ 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 } } - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) init { loadImpl() @@ -149,6 +150,10 @@ class ReaderViewModel @Inject constructor( .onEach { key -> if (key == AppSettings.KEY_READER_SLIDER) notifyStateChanged() }.launchIn(viewModelScope + Dispatchers.Default) + launchJob(Dispatchers.Default) { + val mangaId = mangaFlow.filterNotNull().first().id + shortcutsUpdater.notifyMangaOpened(mangaId) + } } fun reload() { @@ -158,16 +163,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()) } } } @@ -180,9 +183,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), ) } @@ -203,12 +206,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) } } } @@ -224,18 +227,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)) } } @@ -244,7 +247,8 @@ class ReaderViewModel @Inject constructor( val prevJob = stateChangeJob stateChangeJob = launchJob(Dispatchers.Default) { prevJob?.cancelAndJoin() - val pages = content.value?.pages ?: return@launchJob + loadingJob?.join() + val pages = content.value.pages pages.getOrNull(position)?.let { page -> currentState.update { cs -> cs?.copy(chapterId = page.chapterId, page = page.index) @@ -255,12 +259,12 @@ class ReaderViewModel @Inject constructor( return@launchJob } ensureActive() - if (position <= BOUNDS_PAGE_OFFSET) { - loadPrevNextChapter(pages.first().chapterId, isNext = false) - } if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) { loadPrevNextChapter(pages.last().chapterId, isNext = true) } + if (position <= BOUNDS_PAGE_OFFSET) { + loadPrevNextChapter(pages.first().chapterId, isNext = false) + } if (pageLoader.isPrefetchApplicable()) { pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT)) } @@ -276,7 +280,7 @@ 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, @@ -286,7 +290,7 @@ class ReaderViewModel @Inject constructor( percent = computePercent(state.chapterId, state.page), ) bookmarksRepository.addBookmark(bookmark) - onShowToast.emitCall(R.string.bookmark_added) + onShowToast.call(R.string.bookmark_added) } } @@ -296,7 +300,7 @@ class ReaderViewModel @Inject constructor( } bookmarkJob = launchJob { loadingJob?.join() - val manga = checkNotNull(mangaData.value) + val manga = checkNotNull(mangaData.value?.any) val page = checkNotNull(getCurrentPage()) { "Page not found" } bookmarksRepository.removeBookmark(manga.id, page.id) onShowToast.call(R.string.bookmark_removed) @@ -305,44 +309,44 @@ class ReaderViewModel @Inject constructor( 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) } } @AnyThread private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) { + val prevJob = loadingJob loadingJob = launchLoadingJob(Dispatchers.Default) { + prevJob?.join() chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext) - content.emitValue(ReaderContent(chaptersLoader.snapshot(), null)) + content.value = ReaderContent(chaptersLoader.snapshot(), null) } } @@ -356,46 +360,26 @@ 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, 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) @@ -407,23 +391,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 71% 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..d77cdc62c 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 @@ -18,18 +18,20 @@ 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.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 @@ -51,20 +53,20 @@ 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.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() } } @@ -86,22 +88,22 @@ 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.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() } private fun onPreviewChanged(preview: MangaPage?) { @@ -111,18 +113,18 @@ class ColorFilterConfigActivity : .scale(Scale.FILL) .decodeRegion() .tag(preview.source) - .indicator(listOf(binding.progressBefore, binding.progressAfter)) + .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(ShadowViewTarget(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/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt index d5b3e517f..d4568ed2d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigViewModel.kt @@ -1,19 +1,19 @@ 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 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.parser.MangaRepository +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.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 @@ -26,9 +26,9 @@ class ColorFilterConfigViewModel @Inject constructor( 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 colorFilter = MutableStateFlow(null) + val onDismiss = MutableEventFlow() + val preview = MutableStateFlow(null) val isChanged: Boolean get() = colorFilter.value != initialColorFilter @@ -44,13 +44,11 @@ class ColorFilterConfigViewModel @Inject constructor( 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, - ), + preview.value = MangaPage( + id = page.id, + url = url, + preview = page.preview, + source = page.source, ) } } @@ -72,7 +70,7 @@ class ColorFilterConfigViewModel @Inject constructor( fun save() { launchLoadingJob(Dispatchers.Default) { mangaDataRepository.saveColorFilter(manga, colorFilter.value) - onDismiss.emitCall(Unit) + 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/ShadowViewTarget.kt similarity index 100% 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/ShadowViewTarget.kt 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/ReaderConfigBottomSheet.kt similarity index 83% 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/ReaderConfigBottomSheet.kt index 139902b4f..4fa2db3f8 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/ReaderConfigBottomSheet.kt @@ -11,7 +11,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager import androidx.fragment.app.activityViewModels -import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.slider.Slider @@ -19,19 +18,21 @@ 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.BaseBottomSheet +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 @@ -57,12 +58,12 @@ 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 @@ -76,8 +77,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) { @@ -109,7 +110,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)) } } @@ -119,8 +120,8 @@ 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 } } } @@ -148,8 +149,8 @@ class ReaderConfigBottomSheet : } } - override fun onActivityResult(uri: Uri?) { - viewModel.onActivityResult(uri) + override fun onActivityResult(result: Uri?) { + viewModel.onActivityResult(result) dismissAllowingStateLoss() } @@ -157,9 +158,8 @@ class ReaderConfigBottomSheet : val helper = ScreenOrientationHelper(requireActivity()) orientationHelper = helper helper.observeAutoOrientation() - .flowWithLifecycle(lifecycle) .onEach { - binding.buttonScreenRotate.isGone = it + requireViewBinding().buttonScreenRotate.isGone = it }.launchIn(viewLifecycleScope) } 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 100% 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 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/BaseReader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt similarity index 79% rename from app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt index eee0769bd..e2a9a82e4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt @@ -1,24 +1,24 @@ 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" -abstract class BaseReader : BaseFragment() { +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) + override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) var restoredState = savedInstanceState?.getParcelableCompat(KEY_STATE) viewModel.content.observe(viewLifecycleOwner) { 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..21a0f4f41 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.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 100% 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 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 76% 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 1447a4b36..ce228d578 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,32 +1,31 @@ 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 dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async import kotlinx.coroutines.launch 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.core.util.ext.viewLifecycleScope 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.BaseReader 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 @AndroidEntryPoint -class ReversedReaderFragment : BaseReader() { +class ReversedReaderFragment : BaseReaderFragment() { @Inject lateinit var networkState: NetworkState @@ -36,14 +35,13 @@ class ReversedReaderFragment : BaseReader() { 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) + override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) pagerAdapter = ReversedPagesAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, @@ -70,17 +68,18 @@ class ReversedReaderFragment : BaseReader() { override fun onDestroyView() { pagerAdapter = null + requireViewBinding().pager.adapter = null super.onDestroyView() } 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, @@ -100,7 +99,7 @@ class ReversedReaderFragment : BaseReader() { } items.await() ?: return@launch if (position != -1) { - binding.pager.setCurrentItem(position, false) + requireViewBinding().pager.setCurrentItem(position, false) notifyPageChanged(position) } } else { @@ -109,7 +108,7 @@ class ReversedReaderFragment : BaseReader() { } } - 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/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 92% 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 edf8bce98..eb5927fb1 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,15 @@ 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.* +import org.koitharu.kotatsu.util.ext.* open class PageHolder( owner: LifecycleOwner, @@ -31,9 +34,11 @@ 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) + @Suppress("LeakingThis") bindingInfo.buttonErrorDetails.setOnClickListener(this) binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled } 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 75% 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 49b4e1239..c08199d24 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,31 +1,30 @@ 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 dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async import kotlinx.coroutines.launch 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.core.util.ext.viewLifecycleScope 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.BaseReader 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 @AndroidEntryPoint -class PagerReaderFragment : BaseReader() { +class PagerReaderFragment : BaseReaderFragment() { @Inject lateinit var networkState: NetworkState @@ -35,14 +34,13 @@ class PagerReaderFragment : BaseReader() { 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) + override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) pagesAdapter = PagesAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, @@ -69,6 +67,7 @@ class PagerReaderFragment : BaseReader() { override fun onDestroyView() { pagesAdapter = null + requireViewBinding().pager.adapter = null super.onDestroyView() } @@ -83,7 +82,7 @@ class PagerReaderFragment : BaseReader() { } items.await() ?: return@launch if (position != -1) { - binding.pager.setCurrentItem(position, false) + requireViewBinding().pager.setCurrentItem(position, false) notifyPageChanged(position) } } else { @@ -93,13 +92,13 @@ class PagerReaderFragment : BaseReader() { } 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, @@ -107,7 +106,7 @@ class PagerReaderFragment : BaseReader() { } } - 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 77% 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 226961f32..e98d90c94 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,27 +2,27 @@ 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 dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.async import kotlinx.coroutines.launch 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.core.util.ext.viewLifecycleScope 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.BaseReader 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 -class WebtoonReaderFragment : BaseReader() { +class WebtoonReaderFragment : BaseReaderFragment() { @Inject lateinit var networkState: NetworkState @@ -33,13 +33,13 @@ class WebtoonReaderFragment : BaseReader() { 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) + override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) webtoonAdapter = WebtoonAdapter( lifecycleOwner = viewLifecycleOwner, loader = pageLoader, @@ -60,6 +60,7 @@ class WebtoonReaderFragment : BaseReader() { override fun onDestroyView() { webtoonAdapter = null + requireViewBinding().recyclerView.adapter = null super.onDestroyView() } @@ -72,7 +73,7 @@ class WebtoonReaderFragment : BaseReader() { } setItems.await() ?: return@launch if (position != -1) { - with(binding.recyclerView) { + with(requireViewBinding().recyclerView) { firstVisibleItemPosition = position post { (findViewHolderForAdapterPosition(position) as? WebtoonHolder) @@ -87,7 +88,7 @@ class WebtoonReaderFragment : BaseReader() { } } - 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 @@ -104,7 +105,7 @@ class WebtoonReaderFragment : BaseReader() { } override fun switchPageBy(delta: Int) { - with(binding.recyclerView) { + with(requireViewBinding().recyclerView) { if (context.isAnimationsEnabled) { smoothScrollBy(0, (height * 0.9).toInt() * delta, scrollInterpolator) } else { @@ -114,11 +115,11 @@ class WebtoonReaderFragment : BaseReader() { } 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..3246d0a99 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt @@ -0,0 +1,112 @@ +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 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.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.await +import org.koitharu.kotatsu.parsers.util.mimeType +import org.koitharu.kotatsu.reader.domain.PageLoader +import java.util.zip.ZipFile + +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, +) : 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) + okHttpClient.newCall(request).await().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( + private val context: Context, + private val okHttpClient: OkHttpClient, + private val pagesCache: PagesCache, + private val mangaRepositoryFactory: MangaRepository.Factory, + ) : 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, + ) + } + } + + 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..39255497c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -0,0 +1,177 @@ +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.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.list.BoundsScrollListener +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.ScrollListenerInvalidationObserver +import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration +import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar +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.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.SheetPagesBinding +import org.koitharu.kotatsu.list.ui.MangaListSpanResolver +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.reader.ui.thumbnails.adapter.PageThumbnailAdapter +import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.TargetScrollObserver +import org.koitharu.kotatsu.util.LoggingAdapterDataObserver +import javax.inject.Inject + +@AndroidEntryPoint +class PagesThumbnailsSheet : + BaseBottomSheet(), + OnListItemClickListener, + BottomSheetHeaderBar.OnExpansionChangeListener { + + 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) + spanResolver = MangaListSpanResolver(binding.root.resources) + with(binding.headerBar) { + title = viewModel.title + subtitle = null + addOnExpansionChangeListener(this@PagesThumbnailsSheet) + } + 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 + thumbnailsAdapter?.registerAdapterDataObserver( + ScrollListenerInvalidationObserver(this, checkNotNull(scrollListener)), + ) + thumbnailsAdapter?.registerAdapterDataObserver(TargetScrollObserver(this)) + thumbnailsAdapter?.registerAdapterDataObserver(LoggingAdapterDataObserver("THUMB")) + } + viewModel.thumbnails.observe(viewLifecycleOwner) { + thumbnailsAdapter?.setItems(it, listCommitCallback) + } + viewModel.branch.observe(viewLifecycleOwner) { + onExpansionStateChanged(binding.headerBar, binding.headerBar.isExpanded) + } + 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 = ReaderActivity.newIntent(view.context, viewModel.manga, state) + startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) + } + dismiss() + } + + override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { + if (isExpanded) { + headerBar.subtitle = viewModel.branch.value + } else { + headerBar.subtitle = null + } + } + + private inner class ScrollListener : BoundsScrollListener(3, 3) { + + override fun onScrolledToStart(recyclerView: RecyclerView) { + viewModel.loadPrevChapter() + } + + override fun onScrolledToEnd(recyclerView: RecyclerView) { + viewModel.loadNextChapter() + } + } + + 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..30e7ccb75 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsViewModel.kt @@ -0,0 +1,104 @@ +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.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 = requireNotNull(savedStateHandle.get(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 + + val thumbnails = MutableStateFlow>(emptyList()) + val branch = MutableStateFlow(null) + val title = manga.title + + init { + loadingJob = launchJob(Dispatchers.Default) { + chaptersLoader.init(mangaDetails.get()) + chaptersLoader.loadSingleChapter(initialChapterId) + updateList() + } + } + + fun loadPrevChapter() { + if (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 = 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..e1169638a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAdapter.kt @@ -0,0 +1,60 @@ +package org.koitharu.kotatsu.reader.ui.thumbnails.adapter + +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.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()) { + + init { + delegatesManager.addDelegate(ITEM_TYPE_THUMBNAIL, pageThumbnailAD(coil, lifecycleOwner, clickListener)) + .addDelegate(ITEM_TYPE_HEADER, listHeaderAD(null)) + .addDelegate(ITEM_LOADING, loadingFooterAD()) + } + + 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 89% 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..c441f6974 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.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 fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) addMenuProvider(RemoteListMenuProvider()) } 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 88% 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 3c4b6677d..ee0890f4f 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,18 +8,25 @@ 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.StateFlow 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.MangaDataRepository 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.ui.widgets.ChipsView +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.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator import org.koitharu.kotatsu.list.ui.filter.FilterItem @@ -37,9 +43,7 @@ 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 org.koitharu.kotatsu.util.ext.printStackTraceDebug import java.util.LinkedList import javax.inject.Inject @@ -53,7 +57,8 @@ class RemoteListViewModel @Inject constructor( settings: AppSettings, dataRepository: MangaDataRepository, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings), OnFilterChangedListener { + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler), OnFilterChangedListener { val source = savedStateHandle.require(RemoteListFragment.ARG_SOURCE) private val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository @@ -63,12 +68,12 @@ class RemoteListViewModel @Inject constructor( private val listError = MutableStateFlow(null) private var loadingJob: Job? = null - val filterItems: LiveData> + val filterItems: StateFlow> get() = filter.items override val content = combine( mangaList, - listModeFlow, + listMode, createHeaderFlow(), listError, hasNextPage, @@ -83,12 +88,12 @@ class RemoteListViewModel @Inject constructor( list.toUi(this, mode, tagHighlighter) 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() @@ -161,7 +166,7 @@ class RemoteListViewModel @Inject constructor( e.printStackTraceDebug() listError.value = e if (!mangaList.value.isNullOrEmpty()) { - errorEvent.emitCall(e) + errorEvent.call(e) } } } 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 70% 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 607cbc5e1..f9f3b1187 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 @@ -25,11 +22,9 @@ import org.koitharu.kotatsu.scrobbling.kitsu.data.KitsuRepository import org.koitharu.kotatsu.scrobbling.kitsu.domain.KitsuScrobbler 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 @@ -39,57 +34,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 97d93c7b1..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 { @@ -241,6 +245,7 @@ class AniListRepository( descriptionHtml = json.getString("description"), ) + @Suppress("FunctionName") private fun AniListUser(json: JSONObject) = ScrobblerUser( id = json.getLong("id"), nickname = json.getString("name"), 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 90% 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..1d4e04c41 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.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 95% 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..3cd124561 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,9 @@ 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.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 +20,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.util.ext.printStackTraceDebug import java.util.EnumMap abstract class Scrobbler( @@ -53,7 +53,7 @@ abstract class Scrobbler( return repository.loadUser() } - suspend fun logout() { + fun logout() { repository.logout() } 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 81% 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..f1a4fce2b 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 @@ -12,10 +12,15 @@ 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,9 +28,6 @@ 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 @AndroidEntryPoint @@ -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,12 +61,12 @@ 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.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.onLoggedOut.observe(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.isVisible = false return } - binding.imageViewAvatar.isVisible = true - binding.imageViewAvatar.newImageRequest(this, user.avatar) + viewBinding.imageViewAvatar.isVisible = true + viewBinding.imageViewAvatar.newImageRequest(this, user.avatar) ?.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/ScrobblingSelectorBottomSheet.kt similarity index 79% 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/ScrobblingSelectorBottomSheet.kt index 76b5d8de0..65e79be5e 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/ScrobblingSelectorBottomSheet.kt @@ -14,12 +14,17 @@ 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.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener +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,9 +32,6 @@ 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 @@ -50,14 +52,14 @@ 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) 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) @@ -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 -> @@ -133,7 +135,7 @@ class ScrobblingSelectorBottomSheet : return false } viewModel.search(query) - binding.headerBar.menu.findItem(R.id.action_search)?.collapseActionView() + requireViewBinding().headerBar.menu.findItem(R.id.action_search)?.collapseActionView() return true } @@ -149,11 +151,11 @@ 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().headerBar.menu.findItem(R.id.action_search) ?: return menuItem.expandActionView() } @@ -165,8 +167,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().headerBar.inflateMenu(R.menu.opt_shiki_selector) + val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search) searchMenuItem.setOnActionExpandListener(this) val searchView = searchMenuItem.actionView as SearchView searchView.setOnQueryTextListener(this) @@ -179,7 +181,7 @@ class ScrobblingSelectorBottomSheet : private fun initTabs() { val entries = viewModel.availableScrobblers - val tabs = binding.tabs + val tabs = requireViewBinding().tabs if (entries.size <= 1) { tabs.isVisible = false return 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..f322f53c4 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.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 6c4d7af38..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 { @@ -200,6 +204,7 @@ class MALRepository( descriptionHtml = json.getString("synopsis"), ) + @Suppress("FunctionName") private fun MALUser(json: JSONObject) = ScrobblerUser( id = json.getLong("id"), nickname = json.getString("name"), 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 1967249d2..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 { @@ -207,6 +211,7 @@ class ShikimoriRepository( descriptionHtml = json.getString("description_html"), ) + @Suppress("FunctionName") private fun ShikimoriUser(json: JSONObject) = ScrobblerUser( id = json.getLong("id"), nickname = json.getString("nickname"), 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/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index c1063de8c..1dd93c2a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -9,16 +9,16 @@ 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.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat +import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat 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 : @@ -26,7 +26,7 @@ class MangaListActivity : AppBarOwner { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,7 +57,7 @@ class MangaListActivity : } 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/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 08e6db9c3..ff7abf11c 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,11 +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.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -21,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 +34,8 @@ class SearchViewModel @Inject constructor( repositoryFactory: MangaRepository.Factory, settings: AppSettings, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings) { + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler) { private val query = savedStateHandle.require(SearchFragment.ARG_QUERY) private val repository = repositoryFactory.create(savedStateHandle.require(SearchFragment.ARG_SOURCE)) @@ -42,7 +46,7 @@ class SearchViewModel @Inject constructor( override val content = combine( mangaList, - listModeFlow, + listMode, listError, hasNextPage, ) { list, mode, error, hasNext -> @@ -63,12 +67,12 @@ class SearchViewModel @Inject constructor( list.toUi(result, mode, tagHighlighter) 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 79% 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 e16fd5591..c8c75b2dd 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,13 +14,19 @@ 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.service.DownloadService +import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration @@ -32,9 +38,6 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity 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 @@ -58,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) @@ -79,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) @@ -89,14 +90,16 @@ class MultiSearchActivity : viewModel.query.observe(this) { title = it } viewModel.list.observe(this) { adapter.items = it } + 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, ) } @@ -127,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 @@ -139,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 { @@ -162,7 +165,7 @@ class MultiSearchActivity : } R.id.action_save -> { - DownloadService.confirmAndStart(binding.recyclerView, collectSelectedItems()) + viewModel.download(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 100% 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 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 77% 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 d2dc9c5c9..51a273537 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,14 +10,21 @@ 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 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.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingFooter @@ -27,10 +32,8 @@ 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.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.util.ext.printStackTraceDebug import javax.inject.Inject private const val MAX_PARALLELISM = 4 @@ -41,15 +44,17 @@ class MultiSearchViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val settings: AppSettings, private val mangaRepositoryFactory: MangaRepository.Factory, + private val downloadScheduler: DownloadWorker.Scheduler, ) : BaseViewModel() { private var searchJob: Job? = null private val listData = MutableStateFlow>(emptyList()) private val loadingData = MutableStateFlow(false) private var listError = MutableStateFlow(null) + 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, @@ -68,13 +73,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 { @@ -97,7 +102,7 @@ class MultiSearchViewModel @Inject constructor( listError.value = null listData.value = emptyList() loadingData.value = true - query.emitValue(q) + query.value = q searchImpl(q) } catch (e: CancellationException) { throw e @@ -109,6 +114,13 @@ class MultiSearchViewModel @Inject constructor( } } + fun download(items: Set) { + launchJob(Dispatchers.Default) { + downloadScheduler.schedule(items) + onDownloadStarted.call(Unit) + } + } + private suspend fun searchImpl(q: String) = coroutineScope { val sources = settings.getMangaSources(includeHidden = false) val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) 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 86% 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..2fdcf1b90 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 @@ -7,9 +7,9 @@ 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.databinding.ItemListGroupBinding import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration @@ -27,12 +27,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 @@ -47,4 +47,4 @@ fun searchResultsAD( adapter.notifyDataSetChanged() adapter.items = item.list } -} \ 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..a01a2d31f 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) } 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 93% 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..468ffb0fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -16,19 +16,19 @@ 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt index 872caac79..e062e6800 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt @@ -7,20 +7,24 @@ 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.setDefaultValueCompat +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.settings.utils.SliderPreference -import org.koitharu.kotatsu.utils.ext.getStorageName -import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.util.ext.printStackTraceDebug import java.io.File +import java.net.Proxy import javax.inject.Inject @AndroidEntryPoint @@ -35,16 +39,12 @@ class ContentSettingsFragment : @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_DOWNLOADS_PARALLELISM)?.run { - summary = value.toString() - setOnPreferenceChangeListener { preference, newValue -> - preference.summary = newValue.toString() - true - } - } findPreference(AppSettings.KEY_DOH)?.run { entryValues = arrayOf( DoHProvider.NONE, @@ -63,6 +63,7 @@ class ContentSettingsFragment : if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled, ) bindRemoteSourcesSummary() + bindProxySummary() settings.subscribe(this) } @@ -87,9 +88,19 @@ class ContentSettingsFragment : bindRemoteSourcesSummary() } + AppSettings.KEY_DOWNLOADS_WIFI -> { + updateDownloadsConstraints() + } + 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() + } } } @@ -126,4 +137,33 @@ class ContentSettingsFragment : summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total) } } + + 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 { + "$type $address:$port" + } + } + } + + private fun updateDownloadsConstraints() { + val preference = findPreference(AppSettings.KEY_DOWNLOADS_WIFI) + viewLifecycleScope.launch { + try { + preference?.isEnabled = false + withContext(Dispatchers.Default) { + downloadsScheduler.updateConstraints() + } + } catch (e: Exception) { + e.printStackTraceDebug() + } finally { + preference?.isEnabled = true + } + } + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DomainValidator.kt similarity index 63% rename from app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/DomainValidator.kt index 3681a72e7..0ac467edc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/DomainValidator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DomainValidator.kt @@ -1,20 +1,17 @@ package org.koitharu.kotatsu.settings import okhttp3.HttpUrl -import okhttp3.internal.toCanonicalHost import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.EditTextValidator +import org.koitharu.kotatsu.core.util.EditTextValidator class DomainValidator : EditTextValidator() { - private val urlBuilder = HttpUrl.Builder() - override fun validate(text: String): ValidationResult { val trimmed = text.trim() if (trimmed.isEmpty()) { return ValidationResult.Success } - return if (!checkCharacters(trimmed) || trimmed.toCanonicalHost() == null) { + return if (!checkCharacters(trimmed)) { ValidationResult.Failed(context.getString(R.string.invalid_domain_message)) } else { ValidationResult.Success @@ -22,6 +19,12 @@ class DomainValidator : EditTextValidator() { } private fun checkCharacters(value: String): Boolean = runCatching { - urlBuilder.host(value) + val parts = value.split(':') + require(parts.size <= 2) + val urlBuilder = HttpUrl.Builder() + urlBuilder.host(parts.first()) + if (parts.size == 2) { + urlBuilder.port(parts[1].toInt()) + } }.isSuccess } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/HeaderValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/HeaderValidator.kt new file mode 100644 index 000000000..b25be70d6 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/HeaderValidator.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.settings + +import okhttp3.Headers +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.util.EditTextValidator + +class HeaderValidator : EditTextValidator() { + + private val headers = Headers.Builder() + + override fun validate(text: String): ValidationResult { + val trimmed = text.trim() + if (trimmed.isEmpty()) { + return ValidationResult.Success + } + return if (!validateImpl(trimmed)) { + ValidationResult.Failed(context.getString(R.string.invalid_value_message)) + } else { + ValidationResult.Success + } + } + + private fun validateImpl(value: String): Boolean = runCatching { + headers[CommonHeaders.USER_AGENT] = value + }.isSuccess +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt similarity index 78% rename from app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index c0b53620a..34b2fdb37 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -8,20 +8,23 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers 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.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.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 @@ -39,6 +42,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach @Inject lateinit var cookieJar: MutableCookieJar + @Inject + lateinit var cache: Cache + @Inject lateinit var shortcutsUpdater: ShortcutsUpdater @@ -52,6 +58,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES) findPreference(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.THUMBS) + findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindSummaryToHttpCacheSize() findPreference(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref -> viewLifecycleScope.launch { lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED) @@ -90,6 +97,11 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach true } + AppSettings.KEY_HTTP_CACHE_CLEAR -> { + clearHttpCache() + true + } + AppSettings.KEY_UPDATES_FEED_CLEAR -> { viewLifecycleScope.launch { trackerRepo.clearLogs() @@ -131,6 +143,32 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach summary = FileSize.BYTES.format(context, size) } + private fun Preference.bindSummaryToHttpCacheSize() = viewLifecycleScope.launch { + val size = runInterruptible(Dispatchers.IO) { cache.size() } + summary = FileSize.BYTES.format(context, size) + } + + private fun clearHttpCache() { + val preference = findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR) ?: return + val ctx = preference.context.applicationContext + viewLifecycleScope.launch { + try { + preference.isEnabled = false + val size = runInterruptible(Dispatchers.IO) { + cache.evictAll() + cache.size() + } + preference.summary = FileSize.BYTES.format(ctx, size) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + preference.summary = e.getDisplayMessage(ctx.resources) + } finally { + preference.isEnabled = true + } + } + } + private fun clearSearchHistory(preference: Preference) { MaterialAlertDialogBuilder(context ?: return) .setTitle(R.string.clear_search_history) 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..7b86847c4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt @@ -0,0 +1,60 @@ +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 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 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 = null, + ), + ) + 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 + } +} 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/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt similarity index 83% rename from app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt index ad02acf64..7d737ee50 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/RootSettingsFragment.kt @@ -2,11 +2,11 @@ package org.koitharu.kotatsu.settings import android.os.Bundle import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.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/ServicesSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt similarity index 94% 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 5c403355b..77d0fde10 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ServicesSettingsFragment.kt @@ -12,8 +12,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.data.ScrobblerRepository import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService @@ -23,9 +25,7 @@ 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.util.ext.printStackTraceDebug import javax.inject.Inject @AndroidEntryPoint @@ -168,6 +168,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services) { else -> getString(R.string.disabled) } } + findPreference(AppSettings.KEY_SYNC_SETTINGS)?.isEnabled = account != null } } } 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 91% 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 ad800fcd4..9b608aa96 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -18,16 +18,16 @@ 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.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.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,7 +37,7 @@ class SettingsActivity : FragmentManager.OnBackStackChangedListener { override val appBar: AppBarLayout - get() = binding.appbar + get() = viewBinding.appbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -51,7 +51,7 @@ class SettingsActivity : override fun onTitleChanged(title: CharSequence?, color: Int) { super.onTitleChanged(title, color) - binding.collapsingToolbarLayout?.title = title + viewBinding.collapsingToolbarLayout?.title = title } override fun onStart() { @@ -86,7 +86,7 @@ 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) } } @@ -104,11 +104,11 @@ class SettingsActivity : } override fun onWindowInsetsChanged(insets: Insets) { - binding.appbar.updatePadding( + viewBinding.appbar.updatePadding( left = insets.left, right = insets.right, ) - binding.container.updatePadding( + viewBinding.container.updatePadding( left = insets.left, right = insets.right, ) @@ -137,6 +137,7 @@ class SettingsActivity : Intent.ACTION_VIEW -> { when (intent.data?.host) { HOST_ABOUT -> AboutSettingsFragment() + HOST_SYNC_SETTINGS -> SyncSettingsFragment() else -> SettingsHeadersFragment() } } @@ -159,6 +160,7 @@ class SettingsActivity : private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST" private const val EXTRA_SOURCE = "source" private const val HOST_ABOUT = "about" + private const val HOST_SYNC_SETTINGS = "sync-settings" fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsExt.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsExt.kt index 5137fe95e..293f4847d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsExt.kt @@ -44,7 +44,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang EditTextBindListener( inputType = EditorInfo.TYPE_CLASS_TEXT, hint = key.defaultValue, - validator = null, + validator = HeaderValidator(), ), ) setTitle(R.string.user_agent) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt similarity index 89% rename from app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 43327d44a..565b8395d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -12,20 +12,20 @@ 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.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.awaitViewLifecycle +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.requireSerializable +import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope +import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable 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 org.koitharu.kotatsu.util.ext.printStackTraceDebug import javax.inject.Inject @AndroidEntryPoint 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/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt new file mode 100644 index 000000000..60a609307 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SyncSettingsFragment.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.settings + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.FragmentResultListener +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +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 + +@AndroidEntryPoint +class SyncSettingsFragment : BasePreferenceFragment(R.string.sync_settings), FragmentResultListener { + + @Inject + lateinit var syncSettings: SyncSettings + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_sync) + bindHostSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, viewLifecycleOwner, this) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + SyncSettings.KEY_HOST -> { + SyncHostDialogFragment.show(childFragmentManager) + true + } + + else -> super.onPreferenceTreeClick(preference) + } + } + + override fun onFragmentResult(requestKey: String, result: Bundle) { + bindHostSummary() + } + + private fun bindHostSummary() { + val preference = findPreference(SyncSettings.KEY_HOST) ?: return + preference.summary = syncSettings.host + } +} 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 81% 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..1a93c12d9 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,7 +3,6 @@ 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 @@ -12,9 +11,11 @@ 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 +37,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 +68,7 @@ class BackupDialogFragment : AlertDialogFragment() { } private fun onProgressChanged(value: Float) { - with(binding.progressBar) { + with(requireViewBinding().progressBar) { isVisible = true val wasIndeterminate = isIndeterminate isIndeterminate = value < 0 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/BackupSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt index 53ad51cbc..fbeb28594 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt @@ -8,9 +8,9 @@ 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 +import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.util.ext.printStackTraceDebug class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), @@ -18,7 +18,7 @@ class BackupSettingsFragment : private val backupSelectCall = registerForActivityResult( ActivityResultContracts.OpenDocument(), - this + this, ) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -31,17 +31,19 @@ class BackupSettingsFragment : 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 + listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT, ).show() } true } + else -> super.onPreferenceTreeClick(preference) } } @@ -50,4 +52,4 @@ class BackupSettingsFragment : RestoreDialogFragment.newInstance(result ?: return) .show(childFragmentManager, BackupDialogFragment.TAG) } -} \ No newline at end of file +} 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 78% 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..f2d0b200e 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,19 @@ 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.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 +23,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 +53,7 @@ class RestoreDialogFragment : AlertDialogFragment() { } private fun onProgressChanged(value: Float) { - with(binding.progressBar) { + with(requireViewBinding().progressBar) { isVisible = true val wasIndeterminate = isIndeterminate isIndeterminate = value < 0 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 80% 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 167f23750..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,9 +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.textAndVisible fun sourceLocaleAD( listener: SourceLocaleListener, @@ -19,9 +20,6 @@ fun sourceLocaleAD( bind { payloads -> binding.textViewTitle.text = item.title ?: getString(R.string.different_languages) binding.textViewDescription.textAndVisible = item.summary - binding.switchToggle.isChecked = item.isChecked - if (payloads.isEmpty()) { - binding.switchToggle.jumpDrawablesToCurrentState() - } + binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty()) } } 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 67% 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..88a1f64e4 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,8 @@ 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.databinding.ActivitySetupProtectBinding private const val MIN_PASSWORD_LENGTH = 4 @@ -36,29 +37,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) { finishAfterTransition() } viewModel.onPasswordMismatch.observe(this) { - binding.editPassword.error = getString(R.string.passwords_mismatch) + viewBinding.editPassword.error = getString(R.string.passwords_mismatch) } viewModel.onClearText.observe(this) { - binding.editPassword.text?.clear() + 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 +71,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 +81,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 +94,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/sources/SourcesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt similarity index 88% 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..12b20babc 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,9 +16,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.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 @@ -28,8 +31,6 @@ 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 +46,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 +66,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, 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 80% 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 8c424eccd..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,11 +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.getSerializableExtraCompat import javax.inject.Inject import com.google.android.material.R as materialR @@ -41,7 +42,9 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(ActivityBrowserBinding.inflate(layoutInflater)) + if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) { + return + } val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource if (source == null) { finishAfterTransition() @@ -61,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 @@ -77,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 @@ -107,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) @@ -135,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 77% 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 798fa7c33..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,10 +13,12 @@ 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.DownloadsActivity +import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.about.AppUpdateDialog @@ -28,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) @@ -42,7 +44,7 @@ class ToolsFragment : binding.memoryUsageView.setManageButtonOnClickListener(this) viewModel.isIncognitoModeEnabled.observe(viewLifecycleOwner) { - binding.switchIncognito.isChecked = it + binding.switchIncognito.setChecked(it, false) } viewModel.storageUsage.observe(viewLifecycleOwner) { binding.memoryUsageView.bind(it) @@ -74,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 98% 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..51cca1a26 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" @@ -95,24 +96,29 @@ 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) } } + AppSettings.KEY_TRACK_CATEGORIES -> { TrackerCategoriesConfigSheet.show(childFragmentManager) true } + KEY_IGNORE_DOZE -> { startIgnoreDoseActivity(preference.context) true } + else -> super.onPreferenceTreeClick(preference) } } 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 77% 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..942ef03ee 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 @@ -9,9 +9,10 @@ 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.BaseBottomSheet +import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.SheetBaseBinding @AndroidEntryPoint @@ -22,12 +23,12 @@ class TrackerCategoriesConfigSheet : 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) 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 94% 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..950c69c2c 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.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 85% 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 index fe1d3f15c..378133c58 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/utils/AutoCompleteTextViewPreference.kt @@ -10,6 +10,7 @@ import android.widget.EditText import androidx.annotation.ArrayRes import androidx.annotation.AttrRes import androidx.annotation.StyleRes +import androidx.core.content.withStyledAttributes import androidx.preference.EditTextPreference import org.koitharu.kotatsu.R @@ -25,6 +26,12 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor( init { super.setOnBindEditTextListener(autoCompleteBindListener) + context.withStyledAttributes(attrs, R.styleable.AutoCompleteTextViewPreference, defStyleAttr, defStyleRes) { + val entriesId = getResourceId(R.styleable.AutoCompleteTextViewPreference_android_entries, 0) + if (entriesId != 0) { + setEntries(entriesId) + } + } } fun setEntries(@ArrayRes arrayResId: Int) { @@ -55,4 +62,4 @@ class AutoCompleteTextViewPreference @JvmOverloads constructor( } } } -} \ No newline at end of file +} 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/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/shelf/domain/ShelfRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt similarity index 65% 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 7ed1c3161..5fa916e1c 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 @@ -14,37 +10,38 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.toManga -import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory -import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.local.data.LocalManga +import org.koitharu.kotatsu.favourites.data.toMangaList +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( +class ShelfContentObserveUseCase @Inject constructor( private val localMangaRepository: LocalMangaRepository, private val historyRepository: HistoryRepository, private val trackingRepository: TrackingRepository, + private val suggestionRepository: SuggestionRepository, private val db: MangaDatabase, @LocalStorageChanges private val localStorageChanges: SharedFlow, ) { - fun observeShelfContent(): Flow = combine( + operator fun invoke(): Flow = combine( historyRepository.observeAllWithHistory(), observeLocalManga(SortOrder.UPDATED), observeFavourites(), trackingRepository.observeUpdatedManga(), - ) { history, local, favorites, updated -> - ShelfContent(history, favorites, updated, local) + suggestionRepository.observeAll(16), + ) { history, local, favorites, updated, suggestions -> + ShelfContent(history, favorites, updated, local, suggestions) } private fun observeLocalManga(sortOrder: SortOrder): Flow> { @@ -67,30 +64,13 @@ 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>>( categories.map { cat -> val category = cat.toFavouriteCategory() db.favouritesDao.observeAll(category.id, category.order) - .map { category to it.map { x -> x.manga.toManga(x.tags.toMangaTags()) } } + .map { category to it.toMangaList() } }, ) { array -> array.toMap() } } 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 78% 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 c8089e431..3e98c8cd3 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,7 +1,7 @@ -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.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.parsers.model.Manga class ShelfContent( @@ -9,6 +9,7 @@ class ShelfContent( val favourites: Map>, val updated: Map, val local: List, + val suggestions: List, ) { override fun equals(other: Any?): Boolean { @@ -21,8 +22,7 @@ class ShelfContent( if (favourites != other.favourites) return false if (updated != other.updated) return false if (local != other.local) return false - - return true + return suggestions == other.suggestions } override fun hashCode(): Int { @@ -30,6 +30,7 @@ class ShelfContent( result = 31 * result + favourites.hashCode() result = 31 * result + updated.hashCode() result = 31 * result + local.hashCode() + result = 31 * result + suggestions.hashCode() return result } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt new file mode 100644 index 000000000..ecdd14b7d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.shelf.domain.model + +enum class ShelfSection { + + HISTORY, LOCAL, UPDATED, FAVORITES, SUGGESTIONS; +} 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 75% 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 deeaf2c5e..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,15 +13,19 @@ 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 import org.koitharu.kotatsu.favourites.ui.FavouritesActivity import org.koitharu.kotatsu.history.ui.HistoryActivity import org.koitharu.kotatsu.list.ui.ItemSizeResolver @@ -32,8 +36,8 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel +import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity -import org.koitharu.kotatsu.utils.ext.addMenuProvider import javax.inject.Inject @AndroidEntryPoint @@ -54,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( @@ -79,11 +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.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) { @@ -116,6 +121,7 @@ class ShelfFragment : is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category) is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context) is ShelfSectionModel.Local -> MangaListActivity.newIntent(view.context, MangaSource.LOCAL) + is ShelfSectionModel.Suggestions -> SuggestionsActivity.newIntent(view.context) } startActivity(intent) } @@ -132,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 89% 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 7e4919013..a5c5a942e 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,16 +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.download.ui.service.DownloadService +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.FavouriteCategoriesBottomSheet 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, @@ -44,7 +43,9 @@ class ShelfSelectionCallback( ): Boolean { val checkedIds = controller.peekCheckedIds().entries val singleKey = checkedIds.singleOrNull { (_, ids) -> ids.isNotEmpty() }?.key - menu.findItem(R.id.action_remove)?.isVisible = singleKey != null && singleKey !is ShelfSectionModel.Updated + menu.findItem(R.id.action_remove)?.isVisible = singleKey != null && + singleKey !is ShelfSectionModel.Updated && + singleKey !is ShelfSectionModel.Suggestions menu.findItem(R.id.action_save)?.isVisible = singleKey !is ShelfSectionModel.Local return super.onPrepareActionMode(controller, mode, menu) } @@ -68,7 +69,7 @@ class ShelfSelectionCallback( } R.id.action_save -> { - DownloadService.confirmAndStart(recyclerView, collectSelectedItems(controller)) + viewModel.download(collectSelectedItems(controller)) mode.finish() true } @@ -83,6 +84,8 @@ class ShelfSelectionCallback( showDeletionConfirm(ids, mode) return true } + + is ShelfSectionModel.Suggestions -> return false } 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 72% 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 d0ad7dd6b..557801d47 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,24 +1,30 @@ 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.history.data.PROGRESS_NONE +import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -27,41 +33,44 @@ 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, + shelfContentObserveUseCase: ShelfContentObserveUseCase, syncController: SyncController, networkState: NetworkState, ) : BaseViewModel(), ListExtraProvider { - val onActionDone = 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(), - ) { sections, isTrackerEnabled, isConnected, content -> - mapList(content, isTrackerEnabled, sections, isConnected) + 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) { @@ -91,7 +100,7 @@ class ShelfViewModel @Inject constructor( } 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)) } } @@ -101,14 +110,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)) } } @@ -121,12 +130,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) { @@ -144,13 +153,21 @@ class ShelfViewModel @Inject constructor( return result } + fun download(items: Set) { + launchJob(Dispatchers.Default) { + downloadScheduler.schedule(items) + onDownloadStarted.call(Unit) + } + } + private suspend fun mapList( content: ShelfContent, isTrackerEnabled: Boolean, + isSuggestionsEnabled: Boolean, sections: List, isNetworkAvailable: Boolean, ): List { - val result = ArrayList(content.favourites.keys.size + 3) + val result = ArrayList(content.favourites.keys.size + sections.size) if (isNetworkAvailable) { for (section in sections) { when (section) { @@ -161,6 +178,9 @@ class ShelfViewModel @Inject constructor( } ShelfSection.FAVORITES -> mapFavourites(result, content.favourites) + ShelfSection.SUGGESTIONS -> if (isSuggestionsEnabled) { + mapSuggestions(result, content.suggestions) + } } } } else { @@ -180,6 +200,7 @@ class ShelfViewModel @Inject constructor( ShelfSection.LOCAL -> mapLocal(result, content.local) ShelfSection.UPDATED -> Unit ShelfSection.FAVORITES -> Unit + ShelfSection.SUGGESTIONS -> Unit } } } @@ -247,6 +268,19 @@ class ShelfViewModel @Inject constructor( ) } + private suspend fun mapSuggestions( + destination: MutableList, + suggestions: List, + ) { + if (suggestions.isEmpty()) { + return + } + destination += ShelfSectionModel.Suggestions( + items = suggestions.toUi(ListMode.GRID, this, null), + showAllButtonText = R.string.show_all, + ) + } + private suspend fun mapFavourites( destination: MutableList, favourites: Map>, 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 88% 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 c02b7a438..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,9 +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.shelf.domain.model.ShelfSection @SuppressLint("ClickableViewAccessibility") fun shelfSectionAD( @@ -42,10 +43,7 @@ fun shelfSectionAD( bind { payloads -> binding.textViewTitle.setText(item.section.titleResId) - binding.switchToggle.isChecked = item.isChecked - if (payloads.isEmpty()) { - binding.switchToggle.jumpDrawablesToCurrentState() - } + binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty()) } } @@ -65,10 +63,7 @@ fun shelfCategoryAD( bind { payloads -> binding.root.text = item.title - binding.root.isChecked = item.isChecked - if (payloads.isEmpty()) { - binding.root.jumpDrawablesToCurrentState() - } + binding.root.setChecked(item.isChecked, payloads.isNotEmpty()) } } @@ -78,4 +73,5 @@ private val ShelfSection.titleResId: Int ShelfSection.LOCAL -> R.string.local_storage ShelfSection.UPDATED -> R.string.updated ShelfSection.FAVORITES -> R.string.favourites + ShelfSection.SUGGESTIONS -> R.string.suggestions } 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 79% 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 index da3f0ba2c..168087d10 100644 --- 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 @@ -35,9 +35,7 @@ sealed interface ShelfSectionModel : ListModel { other as History if (showAllButtonText != other.showAllButtonText) return false - if (items != other.items) return false - - return true + return items == other.items } override fun hashCode(): Int { @@ -67,9 +65,7 @@ sealed interface ShelfSectionModel : ListModel { if (category != other.category) return false if (showAllButtonText != other.showAllButtonText) return false - if (items != other.items) return false - - return true + return items == other.items } override fun hashCode(): Int { @@ -98,9 +94,7 @@ sealed interface ShelfSectionModel : ListModel { other as Updated if (items != other.items) return false - if (showAllButtonText != other.showAllButtonText) return false - - return true + return showAllButtonText == other.showAllButtonText } override fun hashCode(): Int { @@ -128,9 +122,35 @@ sealed interface ShelfSectionModel : ListModel { other as Local if (items != other.items) return false - if (showAllButtonText != other.showAllButtonText) return false + return showAllButtonText == other.showAllButtonText + } - return true + override fun hashCode(): Int { + var result = items.hashCode() + result = 31 * result + showAllButtonText + return result + } + + override fun toString(): String = key + } + + class Suggestions( + override val items: List, + override val showAllButtonText: Int, + ) : ShelfSectionModel { + + override val key = "suggestions" + + override fun getTitle(resources: Resources) = resources.getString(R.string.suggestions) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Suggestions + + if (items != other.items) return false + return showAllButtonText == other.showAllButtonText } override fun hashCode(): Int { 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 67% 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 index 0f80321a0..cb77f7a68 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -1,6 +1,11 @@ package org.koitharu.kotatsu.suggestions.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 androidx.room.Update import kotlinx.coroutines.flow.Flow @Dao @@ -10,6 +15,10 @@ abstract class SuggestionDao { @Query("SELECT * FROM suggestions ORDER BY relevance DESC") abstract fun observeAll(): Flow> + @Transaction + @Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit") + abstract fun observeAll(limit: Int): Flow> + @Query("SELECT COUNT(*) FROM suggestions") abstract suspend fun count(): Int @@ -28,4 +37,4 @@ abstract class SuggestionDao { insert(entity) } } -} \ No newline at end of file +} 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 86% 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 f0afaf429..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 @@ -1,16 +1,16 @@ package org.koitharu.kotatsu.suggestions.domain import androidx.room.withTransaction -import javax.inject.Inject import kotlinx.coroutines.flow.Flow 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.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( private val db: MangaDatabase, @@ -22,6 +22,12 @@ class SuggestionRepository @Inject constructor( } } + fun observeAll(limit: Int): Flow> { + return db.suggestionDao.observeAll(limit).mapItems { + it.manga.toManga(it.tags.toMangaTags()) + } + } + suspend fun clear() { db.suggestionDao.deleteAll() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt new file mode 100644 index 000000000..a408dd722 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/TagsBlacklist.kt @@ -0,0 +1,31 @@ +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 + +class TagsBlacklist( + private val tags: Set, + private val threshold: Float, +) { + + fun isNotEmpty() = tags.isNotEmpty() + + operator fun contains(manga: Manga): Boolean { + if (tags.isEmpty()) { + return false + } + for (mangaTag in manga.tags) { + for (tagTitle in tags) { + if (mangaTag.title.almostEquals(tagTitle, threshold)) { + return true + } + } + } + return false + } + + operator fun contains(tag: MangaTag): Boolean = tags.any { + it.almostEquals(tag.title, threshold) + } +} 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 77% 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 1741936f2..5eff7818f 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,20 +3,23 @@ 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.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 @@ -24,11 +27,12 @@ class SuggestionsViewModel @Inject constructor( repository: SuggestionRepository, settings: AppSettings, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings) { + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler) { override val content = combine( repository.observeAll(), - listModeFlow, + listMode, ) { list, mode -> when { list.isEmpty() -> listOf( @@ -48,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/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt new file mode 100644 index 000000000..e5770a597 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -0,0 +1,353 @@ +package org.koitharu.kotatsu.suggestions.ui + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.annotation.FloatRange +import androidx.core.app.NotificationCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.text.HtmlCompat +import androidx.core.text.buildSpannedString +import androidx.core.text.parseAsHtml +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import coil.ImageLoader +import coil.request.ImageRequest +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +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.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.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.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.ui.ReaderActivity +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.util.ext.printStackTraceDebug +import java.util.concurrent.TimeUnit +import kotlin.math.pow +import kotlin.random.Random + +@HiltWorker +class SuggestionsWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val coil: ImageLoader, + private val suggestionRepository: SuggestionRepository, + private val historyRepository: HistoryRepository, + private val favouritesRepository: FavouritesRepository, + private val appSettings: AppSettings, + private val mangaRepositoryFactory: MangaRepository.Factory, +) : 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) + return Result.success(outputData) + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val title = applicationContext.getString(R.string.suggestions_updating) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + WORKER_CHANNEL_ID, + title, + NotificationManager.IMPORTANCE_LOW, + ) + channel.setShowBadge(false) + channel.enableVibration(false) + channel.setSound(null, null) + channel.enableLights(false) + manager.createNotificationChannel(channel) + } + + val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) + .setContentTitle(title) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setDefaults(0) + .setOngoing(true) + .setSilent(true) + .setProgress(0, 0, true) + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) + .build() + + return ForegroundInfo(WORKER_NOTIFICATION_ID, notification) + } + + private suspend fun doWorkImpl(): Int { + val seed = ( + historyRepository.getList(0, 20) + + favouritesRepository.getLastManga(20) + ).distinctById() + val sources = appSettings.getMangaSources(includeHidden = false) + if (seed.isEmpty() || sources.isEmpty()) { + return 0 + } + val tagsBlacklist = TagsBlacklist(appSettings.suggestionsTagsBlacklist, TAG_EQ_THRESHOLD) + val tags = seed.flatMap { it.tags.map { x -> x.title } }.takeMostFrequent(10) + + val producer = channelFlow { + for (it in sources.shuffled()) { + launch { + send(getList(it, tags, tagsBlacklist)) + } + } + } + val suggestions = producer + .flatten() + .take(MAX_RAW_RESULTS) + .map { manga -> + MangaSuggestion( + manga = manga, + relevance = computeRelevance(manga.tags, tags), + ) + }.toList() + .sortedBy { it.relevance } + .take(MAX_RESULTS) + suggestionRepository.replace(suggestions) + if (appSettings.isSuggestionsNotificationAvailable) { + for (i in 0..3) { + try { + val manga = suggestions[Random.nextInt(0, suggestions.size / 3)] + val details = mangaRepositoryFactory.create(manga.manga.source) + .getDetails(manga.manga) + if (details.rating > 0 && details.rating < RATING_MIN) { + continue + } + if (details.isNsfw && appSettings.isSuggestionsExcludeNsfw) { + continue + } + if (details in tagsBlacklist) { + continue + } + showNotification(details) + break + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + e.printStackTraceDebug() + } + } + } + return suggestions.size + } + + private suspend fun getList( + source: MangaSource, + tags: List, + blacklist: TagsBlacklist, + ): List = runCatchingCancellable { + val repository = mangaRepositoryFactory.create(source) + val availableOrders = repository.sortOrders + val order = preferredSortOrders.first { it in availableOrders } + val availableTags = repository.getTags() + val tag = tags.firstNotNullOfOrNull { title -> + availableTags.find { x -> x.title.almostEquals(title, TAG_EQ_THRESHOLD) } + } + val list = repository.getList(0, setOfNotNull(tag), order).asArrayList() + if (appSettings.isSuggestionsExcludeNsfw) { + list.removeAll { it.isNsfw } + } + if (blacklist.isNotEmpty()) { + list.removeAll { manga -> manga in blacklist } + } + list.shuffle() + list.take(MAX_SOURCE_RESULTS) + }.onFailure { + it.printStackTraceDebug() + }.getOrDefault(emptyList()) + + private suspend fun showNotification(manga: Manga) { + val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + MANGA_CHANNEL_ID, + applicationContext.getString(R.string.suggestions), + NotificationManager.IMPORTANCE_DEFAULT, + ) + channel.description = applicationContext.getString(R.string.suggestions_summary) + channel.enableLights(true) + channel.setShowBadge(true) + manager.createNotificationChannel(channel) + } + val id = manga.url.hashCode() + val title = applicationContext.getString(R.string.suggestion_manga, manga.title) + val builder = NotificationCompat.Builder(applicationContext, MANGA_CHANNEL_ID) + val tagsText = manga.tags.joinToString(", ") { it.title } + with(builder) { + setContentText(tagsText) + setContentTitle(title) + setLargeIcon( + coil.execute( + ImageRequest.Builder(applicationContext) + .data(manga.coverUrl) + .tag(manga.source) + .build(), + ).toBitmapOrNull(), + ) + setSmallIcon(R.drawable.ic_stat_suggestion) + val description = manga.description?.parseAsHtml(HtmlCompat.FROM_HTML_MODE_COMPACT) + if (!description.isNullOrBlank()) { + val style = NotificationCompat.BigTextStyle() + style.bigText( + buildSpannedString { + append(tagsText) + appendLine() + append(description) + }, + ) + style.setBigContentTitle(title) + setStyle(style) + } + val intent = DetailsActivity.newIntent(applicationContext, manga) + setContentIntent( + PendingIntentCompat.getActivity( + applicationContext, + id, + intent, + PendingIntent.FLAG_UPDATE_CURRENT, + false, + ), + ) + setAutoCancel(true) + setCategory(NotificationCompat.CATEGORY_RECOMMENDATION) + setVisibility(if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC) + setShortcutId(manga.id.toString()) + priority = NotificationCompat.PRIORITY_DEFAULT + + addAction( + R.drawable.ic_read, + applicationContext.getString(R.string.read), + PendingIntentCompat.getActivity( + applicationContext, + id + 2, + ReaderActivity.newIntent(applicationContext, manga), + 0, + false, + ), + ) + + addAction( + R.drawable.ic_suggestion, + applicationContext.getString(R.string.more), + PendingIntentCompat.getActivity( + applicationContext, + 0, + SuggestionsActivity.newIntent(applicationContext), + 0, + false, + ), + ) + } + manager.notify(TAG, id, builder.build()) + } + + @FloatRange(from = 0.0, to = 1.0) + private fun computeRelevance(mangaTags: Set, allTags: List): Float { + val maxWeight = (allTags.size + allTags.size + 1 - mangaTags.size) * mangaTags.size / 2.0 + val weight = mangaTags.sumOf { tag -> + val index = allTags.inexactIndexOf(tag.title, TAG_EQ_THRESHOLD) + if (index < 0) 0 else allTags.size - index + } + return (weight / maxWeight).pow(2.0).toFloat() + } + + private fun Iterable.inexactIndexOf(element: String, threshold: Float): Int { + forEachIndexed { i, t -> + if (t.almostEquals(element, threshold)) { + return i + } + } + return -1 + } + + companion object { + + private const val TAG = "suggestions" + private const val TAG_ONESHOT = "suggestions_oneshot" + private const val DATA_COUNT = "count" + private const val WORKER_CHANNEL_ID = "suggestion_worker" + private const val MANGA_CHANNEL_ID = "suggestions" + private const val WORKER_NOTIFICATION_ID = 36 + private const val MAX_RESULTS = 80 + private const val MAX_SOURCE_RESULTS = 14 + private const val MAX_RAW_RESULTS = 200 + private const val TAG_EQ_THRESHOLD = 0.4f + private const val RATING_MIN = 0.5f + + private val preferredSortOrders = listOf( + SortOrder.UPDATED, + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.RATING, + ) + + fun setup(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + val request = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) + .setConstraints(constraints) + .addTag(TAG) + .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + } + + fun startNow(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(TAG_ONESHOT) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + WorkManager.getInstance(context) + .enqueue(request) + } + } +} 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 60% 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 69d2e9844..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 @@ -1,31 +1,29 @@ package org.koitharu.kotatsu.sync.data -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.Reusable import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject -import org.koitharu.kotatsu.R 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( - @ApplicationContext context: Context, - private val okHttpClient: OkHttpClient, + @BaseHttpClient private val okHttpClient: OkHttpClient, ) { - private val baseUrl = context.getString(R.string.url_sync_server) - - suspend fun authenticate(email: String, password: String): String { + suspend fun authenticate(host: String, email: String, password: String): String { val body = JSONObject( mapOf("email" to email, "password" to password), ).toRequestBody() + val scheme = getScheme(host) val request = Request.Builder() - .url("$baseUrl/auth") + .url("$scheme://$host/auth") .post(body) .build() val response = okHttpClient.newCall(request).await() @@ -37,4 +35,13 @@ class SyncAuthApi @Inject constructor( throw SyncApiException(message, code) } } + + private suspend fun getScheme(host: String): String { + val request = Request.Builder() + .url("http://$host/") + .head() + .build() + val response = okHttpClient.newCall(request).await() + return response.request.url.scheme + } } 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 84% 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 index 02ab48f3f..dc660e381 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthenticator.kt @@ -9,10 +9,12 @@ import okhttp3.Request import okhttp3.Response import okhttp3.Route import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.network.CommonHeaders class SyncAuthenticator( context: Context, private val account: Account, + private val syncSettings: SyncSettings, private val authApi: SyncAuthApi, ) : Authenticator { @@ -23,13 +25,14 @@ class SyncAuthenticator( val newToken = tryRefreshToken() ?: return null accountManager.setAuthToken(account, tokenType, newToken) return response.request.newBuilder() - .header("Authorization", "Bearer $newToken") + .header(CommonHeaders.AUTHORIZATION, "Bearer $newToken") .build() } private fun tryRefreshToken() = runCatching { runBlocking { authApi.authenticate( + syncSettings.host, account.name, accountManager.getPassword(account), ) 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 88% 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 index 0a9cc0db4..bcc677e6c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncInterceptor.kt @@ -8,6 +8,7 @@ import okhttp3.Response import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.db.DATABASE_VERSION +import org.koitharu.kotatsu.core.network.CommonHeaders class SyncInterceptor( context: Context, @@ -21,10 +22,10 @@ class SyncInterceptor( val token = accountManager.peekAuthToken(account, tokenType) val requestBuilder = chain.request().newBuilder() if (token != null) { - requestBuilder.header("Authorization", "Bearer $token") + requestBuilder.header(CommonHeaders.AUTHORIZATION, "Bearer $token") } requestBuilder.header("X-App-Version", BuildConfig.VERSION_CODE.toString()) requestBuilder.header("X-Db-Version", DATABASE_VERSION.toString()) return chain.proceed(requestBuilder.build()) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt new file mode 100644 index 000000000..7e211e3d4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.sync.data + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.WorkerThread +import dagger.Reusable +import dagger.hilt.android.qualifiers.ApplicationContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import javax.inject.Inject + +@Reusable +class SyncSettings( + context: Context, + private val account: Account?, +) { + + @Inject + constructor(@ApplicationContext context: Context) : this( + context, + AccountManager.get(context) + .getAccountsByType(context.getString(R.string.account_type_sync)) + .firstOrNull(), + ) + + private val accountManager = AccountManager.get(context) + private val defaultHost = context.getString(R.string.sync_host_default) + + @get:WorkerThread + @set:WorkerThread + var host: String + get() = account?.let { + accountManager.getUserData(it, KEY_HOST) + }.ifNullOrEmpty { defaultHost } + set(value) { + account?.let { + accountManager.setUserData(it, KEY_HOST, value) + } + } + + companion object { + + const val KEY_HOST = "host" + } +} 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 76% 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 index e16e1241c..b1581e1d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/domain/SyncAuthResult.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.sync.domain class SyncAuthResult( + val host: String, val email: String, val password: String, val token: String, @@ -12,17 +13,17 @@ class SyncAuthResult( other as SyncAuthResult + if (host != other.host) return false if (email != other.email) return false if (password != other.password) return false - if (token != other.token) return false - - return true + return token == other.token } override fun hashCode(): Int { - var result = email.hashCode() + var result = host.hashCode() + result = 31 * result + email.hashCode() result = 31 * result + password.hashCode() result = 31 * result + token.hashCode() return result } -} \ No newline at end of file +} 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 85% 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 60128fb84..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 @@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread import androidx.core.content.contentValuesOf import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.R @@ -22,15 +23,18 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.TABLE_MANGA import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS 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.utils.GZipInterceptor -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 org.koitharu.kotatsu.sync.data.SyncSettings import java.util.concurrent.TimeUnit private const val FIELD_TIMESTAMP = "timestamp" @@ -41,20 +45,26 @@ private const val FIELD_TIMESTAMP = "timestamp" @WorkerThread class SyncHelper( context: Context, - account: Account, + private val account: Account, private val provider: ContentProviderClient, ) { private val authorityHistory = context.getString(R.string.sync_authority_history) private val authorityFavourites = context.getString(R.string.sync_authority_favourites) + private val settings = SyncSettings(context, account) private val httpClient = OkHttpClient.Builder() - .authenticator(SyncAuthenticator(context, account, SyncAuthApi(context, OkHttpClient()))) + .authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient()))) .addInterceptor(SyncInterceptor(context, account)) .addInterceptor(GZipInterceptor()) .build() - private val baseUrl = context.getString(R.string.url_sync_server) + private val baseUrl: String by lazy { + val host = settings.host + val scheme = getScheme(host) + "$scheme://$host" + } private val defaultGcPeriod: Long // gc period if sync enabled get() = TimeUnit.DAYS.toMillis(4) + private val logger = LoggersModule.provideSyncLogger(context, AppSettings(context)) fun syncFavourites(syncResult: SyncResult) { val data = JSONObject() @@ -65,7 +75,7 @@ class SyncHelper( .url("$baseUrl/resource/$TABLE_FAVOURITES") .post(data.toRequestBody()) .build() - val response = httpClient.newCall(request).execute().parseJsonOrNull() + val response = httpClient.newCall(request).execute().log().parseJsonOrNull() if (response != null) { val timestamp = response.getLong(FIELD_TIMESTAMP) val categoriesResult = @@ -87,7 +97,7 @@ class SyncHelper( .url("$baseUrl/resource/$TABLE_HISTORY") .post(data.toRequestBody()) .build() - val response = httpClient.newCall(request).execute().parseJsonOrNull() + val response = httpClient.newCall(request).execute().log().parseJsonOrNull() if (response != null) { val result = upsertHistory( json = response.getJSONArray(TABLE_HISTORY), @@ -99,6 +109,19 @@ class SyncHelper( gcHistory() } + fun onError(e: Throwable) { + if (logger.isEnabled) { + logger.log("Sync error", e) + } + } + + fun onSyncComplete(result: SyncResult) { + if (logger.isEnabled) { + logger.log("Sync finshed: ${result.toDebugString()}") + logger.flushBlocking() + } + } + private fun upsertHistory(json: JSONArray, timestamp: Long): Array { val uri = uri(authorityHistory, TABLE_HISTORY) val operations = ArrayList() @@ -257,6 +280,15 @@ class SyncHelper( return requireNotNull(tag) } + private fun getScheme(host: String): String { + val request = Request.Builder() + .url("http://$host/") + .head() + .build() + val response = httpClient.newCall(request).execute() + return response.request.url.scheme + } + private fun gcFavourites() { val deletedAt = System.currentTimeMillis() - defaultGcPeriod val selection = "deleted_at != 0 AND deleted_at < ?" @@ -283,4 +315,10 @@ class SyncHelper( private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray + + private fun Response.log() = apply { + if (logger.isEnabled) { + logger.log("$code ${request.url}") + } + } } 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 61% 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 f08a41404..f26b3dd2d 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 @@ -8,24 +8,28 @@ import android.text.Editable import android.text.TextWatcher import android.view.View import android.widget.Button +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.core.graphics.Insets -import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.FragmentResultListener import androidx.transition.Fade 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 { +class SyncAuthActivity : BaseActivity(), View.OnClickListener, FragmentResultListener { private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null private var resultBundle: Bundle? = null @@ -39,25 +43,31 @@ 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.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) { + onAccountAlreadyExists() + } + supportFragmentManager.setFragmentResultListener(SyncHostDialogFragment.REQUEST_KEY, this, this) pageBackCallback.update() } 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, @@ -73,24 +83,37 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi } R.id.button_next -> { - binding.switcher.showNext() + viewBinding.groupLogin.isVisible = false + viewBinding.groupPassword.isVisible = true pageBackCallback.update() + viewBinding.editPassword.requestFocus() } R.id.button_back -> { - binding.switcher.showPrevious() + viewBinding.groupPassword.isVisible = false + viewBinding.groupLogin.isVisible = true pageBackCallback.update() + 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(), ) } + + R.id.button_settings -> { + SyncHostDialogFragment.show(supportFragmentManager) + } } } + override fun onFragmentResult(requestKey: String, result: Bundle) { + val host = result.getString(SyncHostDialogFragment.KEY_HOST) ?: return + viewModel.host.value = host + } + override fun finish() { accountAuthenticatorResponse?.let { response -> resultBundle?.also { @@ -101,12 +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.switcher.isGone = isLoading - binding.layoutProgress.isVisible = isLoading + TransitionManager.beginDelayedTransition(viewBinding.root, Fade()) + viewBinding.layoutProgress.isVisible = isLoading pageBackCallback.update() } @@ -121,8 +143,10 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi private fun onTokenReceived(authResult: SyncAuthResult) { val am = AccountManager.get(this) val account = Account(authResult.email, getString(R.string.account_type_sync)) + val userdata = Bundle(1) + userdata.putString(SyncSettings.KEY_HOST, authResult.host) val result = Bundle() - if (am.addAccountExplicitly(account, authResult.password, Bundle())) { + if (am.addAccountExplicitly(account, authResult.password, userdata)) { result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name) result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) result.putString(AccountManager.KEY_AUTHTOKEN, authResult.token) @@ -135,6 +159,16 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi finish() } + private fun onAccountAlreadyExists() { + Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_SHORT) + .show() + accountAuthenticatorResponse?.onError( + AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, + getString(R.string.account_already_exists), + ) + super.finishAfterTransition() + } + private class EmailTextWatcher( private val button: Button, ) : TextWatcher { @@ -168,12 +202,14 @@ class SyncAuthActivity : BaseActivity(), View.OnClickLi private inner class PageBackCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - binding.switcher.showPrevious() + viewBinding.groupLogin.isVisible = true + viewBinding.groupPassword.isVisible = false + viewBinding.editEmail.requestFocus() update() } fun update() { - isEnabled = binding.switcher.isVisible && binding.switcher.displayedChild > 0 + isEnabled = !viewBinding.layoutProgress.isVisible && viewBinding.groupPassword.isVisible } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt new file mode 100644 index 000000000..fd02134fa --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncAuthViewModel.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.sync.ui + +import android.accounts.AccountManager +import android.content.Context +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.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 javax.inject.Inject + +@HiltViewModel +class SyncAuthViewModel @Inject constructor( + @ApplicationContext context: Context, + private val api: SyncAuthApi, +) : BaseViewModel() { + + val onAccountAlreadyExists = MutableEventFlow() + val onTokenObtained = MutableEventFlow() + val host = MutableStateFlow("") + + private val defaultHost = context.getString(R.string.sync_host_default) + + init { + launchJob(Dispatchers.Default) { + val am = AccountManager.get(context) + val accounts = am.getAccountsByType(context.getString(R.string.account_type_sync)) + if (accounts.isNotEmpty()) { + onAccountAlreadyExists.call(Unit) + } + } + } + + fun obtainToken(email: String, password: String) { + val hostValue = host.value.ifNullOrEmpty { defaultHost } + launchLoadingJob(Dispatchers.Default) { + val token = api.authenticate(hostValue, email, password) + 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/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt new file mode 100644 index 000000000..735adb19a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncHostDialogFragment.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.sync.ui + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.widget.ArrayAdapter +import androidx.core.os.bundleOf +import androidx.core.view.updateLayoutParams +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.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding +import org.koitharu.kotatsu.settings.DomainValidator +import org.koitharu.kotatsu.sync.data.SyncSettings +import javax.inject.Inject + +@AndroidEntryPoint +class SyncHostDialogFragment : AlertDialogFragment(), + DialogInterface.OnClickListener { + + @Inject + lateinit var syncSettings: SyncSettings + + override fun onCreateViewBinding( + inflater: LayoutInflater, + container: ViewGroup? + ) = PreferenceDialogAutocompletetextviewBinding.inflate(inflater, container, false) + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .setCancelable(false) + .setTitle(R.string.server_address) + } + + override fun onViewBindingCreated( + binding: PreferenceDialogAutocompletetextviewBinding, + savedInstanceState: Bundle? + ) { + super.onViewBindingCreated(binding, savedInstanceState) + binding.message.updateLayoutParams { + topMargin = binding.root.resources.getDimensionPixelOffset(R.dimen.screen_padding) + bottomMargin = topMargin + } + binding.message.setText(R.string.sync_host_description) + 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(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries)) + binding.dropdown.setOnClickListener { + editText.showDropDown() + } + DomainValidator().attachToEditText(editText) + } + + override fun onClick(dialog: DialogInterface, which: Int) { + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + val result = requireViewBinding().edit.text?.toString().orEmpty() + syncSettings.host = result + parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_HOST to result)) + } + } + dialog.dismiss() + } + + companion object { + + private const val TAG = "SyncHostDialogFragment" + const val REQUEST_KEY = "sync_host" + const val KEY_HOST = "host" + + fun show(fm: FragmentManager) = SyncHostDialogFragment().show(fm, TAG) + } +} 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 88% 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 index eabd8cd0e..3e9d75c85 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/ui/SyncProvider.kt @@ -14,8 +14,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import org.koitharu.kotatsu.core.db.* -import org.koitharu.kotatsu.core.logs.FileLogger -import org.koitharu.kotatsu.core.logs.SyncLogger import java.util.concurrent.Callable abstract class SyncProvider : ContentProvider() { @@ -24,7 +22,6 @@ abstract class SyncProvider : ContentProvider() { EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java) } private val database by lazy { entryPoint.database } - private val logger by lazy { entryPoint.logger } private val supportedTables = setOf( TABLE_FAVOURITES, @@ -52,7 +49,6 @@ abstract class SyncProvider : ContentProvider() { .selection(selection, selectionArgs) .orderBy(sortOrder) .create() - logger.log("query: ${sqlQuery.sql} (${selectionArgs.contentToString()})") return database.openHelper.readableDatabase.query(sqlQuery) } @@ -65,7 +61,6 @@ abstract class SyncProvider : ContentProvider() { if (values == null || table == null) { return null } - logger.log { "insert: $table [$values]" } val db = database.openHelper.writableDatabase if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) { db.update(table, values) @@ -75,7 +70,6 @@ abstract class SyncProvider : ContentProvider() { override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { val table = getTableName(uri) ?: return 0 - logger.log { "delete: $table ($selection) : (${selectionArgs.contentToString()})" } return database.openHelper.writableDatabase.delete(table, selection, selectionArgs) } @@ -84,7 +78,6 @@ abstract class SyncProvider : ContentProvider() { if (values == null || table == null) { return 0 } - logger.log { "update: $table ($selection) : (${selectionArgs.contentToString()}) [$values]" } return database.openHelper.writableDatabase .update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs) } @@ -127,8 +120,5 @@ abstract class SyncProvider : ContentProvider() { interface SyncProviderEntryPoint { val database: MangaDatabase - - @get:SyncLogger - val logger: FileLogger } } 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 80% 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 cdeacf2db..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) { @@ -28,6 +28,10 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont runCatchingCancellable { syncHelper.syncFavourites(syncResult) SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) - }.onFailure(syncResult::onError) + }.onFailure { e -> + syncResult.onError(e) + syncHelper.onError(e) + } + syncHelper.onSyncComplete(syncResult) } } 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 80% 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 d56529bb1..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) { @@ -28,6 +28,10 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context runCatchingCancellable { syncHelper.syncHistory(syncResult) SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) - }.onFailure(syncResult::onError) + }.onFailure { e -> + syncResult.onError(e) + syncHelper.onError(e) + } + syncHelper.onSyncComplete(syncResult) } } 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 100% 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 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 100% 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 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 81% 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..2e9cfd162 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.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onFeedCleared.observe(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 91% 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..64963e946 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 @@ -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.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorFooterAD @@ -13,6 +13,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 @@ -44,6 +45,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 81% 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 1ebe7b8cd..a3dffc059 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,15 +3,20 @@ 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.history.domain.HistoryRepository -import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.core.util.ext.onFirst +import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -22,8 +27,6 @@ import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.TrackingRepository -import org.koitharu.kotatsu.utils.asFlowLiveData -import org.koitharu.kotatsu.utils.ext.onFirst import javax.inject.Inject @HiltViewModel @@ -32,11 +35,12 @@ class UpdatesViewModel @Inject constructor( private val settings: AppSettings, private val historyRepository: HistoryRepository, private val tagHighlighter: MangaTagHighlighter, -) : MangaListViewModel(settings) { + downloadScheduler: DownloadWorker.Scheduler, +) : MangaListViewModel(settings, downloadScheduler) { override val content = combine( repository.observeUpdatedManga(), - listModeFlow, + listMode, ) { mangaMap, mode -> when { mangaMap.isEmpty() -> listOf( @@ -56,7 +60,7 @@ 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 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 93% 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 a47e8a53a..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 @@ -75,6 +76,7 @@ class TrackWorker @AssistedInject constructor( } finally { withContext(NonCancellable) { logger.flush() + notificationManager.cancel(WORKER_NOTIFICATION_ID) } } } @@ -83,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()) { @@ -179,8 +179,8 @@ class TrackWorker @AssistedInject constructor( ), ) setAutoCancel(true) + setCategory(NotificationCompat.CATEGORY_PROMO) setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC) - color = colorPrimary setShortcutId(manga.id.toString()) priority = NotificationCompat.PRIORITY_DEFAULT if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { @@ -216,13 +216,13 @@ class TrackWorker @AssistedInject constructor( val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) .setContentTitle(title) .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_SERVICE) .setDefaults(0) - .setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)) + .setOngoing(true) .setSilent(true) .setProgress(0, 0, true) .setSmallIcon(android.R.drawable.stat_notify_sync) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) - .setOngoing(true) .build() return ForegroundInfo(WORKER_NOTIFICATION_ID, notification) } @@ -264,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 89% 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 c023f9d05..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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.widget.RemoteViews import android.widget.RemoteViewsService +import androidx.core.graphics.drawable.toBitmap import coil.ImageLoader import coil.executeBlocking import coil.request.ImageRequest @@ -11,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.requireBitmap class RecentListFactory( private val context: Context, @@ -56,7 +57,7 @@ class RecentListFactory( .tag(item.source) .transformations(transformation) .build(), - ).requireBitmap() + ).getDrawableOrThrow().toBitmap() }.onSuccess { cover -> views.setImageViewBitmap(R.id.imageView_cover, cover) }.onFailure { 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 92% 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 f3dd62b87..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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.widget.RemoteViews import android.widget.RemoteViewsService +import androidx.core.graphics.drawable.toBitmap import coil.ImageLoader import coil.executeBlocking import coil.request.ImageRequest @@ -11,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.requireBitmap class ShelfListFactory( private val context: Context, @@ -67,7 +68,7 @@ class ShelfListFactory( .tag(item.source) .transformations(transformation) .build(), - ).requireBitmap() + ).getDrawableOrThrow().toBitmap() }.onSuccess { cover -> views.setImageViewBitmap(R.id.imageView_cover, cover) }.onFailure { 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/color-v23/tab_indicator_foreground.xml b/app/src/main/res/color-v23/tab_indicator_foreground.xml deleted file mode 100644 index 5f1b0284c..000000000 --- a/app/src/main/res/color-v23/tab_indicator_foreground.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color-v23/toolbar_background_scrim.xml b/app/src/main/res/color-v23/toolbar_background_scrim.xml deleted file mode 100644 index 2b5794201..000000000 --- a/app/src/main/res/color-v23/toolbar_background_scrim.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/color/tab_indicator_foreground.xml b/app/src/main/res/color/tab_indicator_foreground.xml deleted file mode 100644 index cf8bd48e8..000000000 --- a/app/src/main/res/color/tab_indicator_foreground.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/toolbar_background_scrim.xml b/app/src/main/res/color/toolbar_background_scrim.xml deleted file mode 100644 index 58c5c9484..000000000 --- a/app/src/main/res/color/toolbar_background_scrim.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v24/ic_stat_paused.xml b/app/src/main/res/drawable-anydpi-v24/ic_stat_paused.xml new file mode 100644 index 000000000..55d7a60db --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_stat_paused.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi-v24/ic_stat_suggestion.xml b/app/src/main/res/drawable-anydpi-v24/ic_stat_suggestion.xml new file mode 100644 index 000000000..3da7d00e0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_stat_suggestion.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_stat_paused.png b/app/src/main/res/drawable-hdpi/ic_stat_paused.png new file mode 100644 index 000000000..e42a3d68c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_paused.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_suggestion.png b/app/src/main/res/drawable-hdpi/ic_stat_suggestion.png new file mode 100644 index 000000000..76ab52d8a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_suggestion.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_paused.png b/app/src/main/res/drawable-mdpi/ic_stat_paused.png new file mode 100644 index 000000000..979fc6fc3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_paused.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_suggestion.png b/app/src/main/res/drawable-mdpi/ic_stat_suggestion.png new file mode 100644 index 000000000..7fe8da107 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_suggestion.png differ diff --git a/app/src/main/res/drawable-v23/tab_rounded_rectangle.xml b/app/src/main/res/drawable-v23/tab_rounded_rectangle.xml deleted file mode 100644 index 724a112c6..000000000 --- a/app/src/main/res/drawable-v23/tab_rounded_rectangle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v23/tab_selector_drawable.xml b/app/src/main/res/drawable-v23/tab_selector_drawable.xml deleted file mode 100644 index 3ef15feb1..000000000 --- a/app/src/main/res/drawable-v23/tab_selector_drawable.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_paused.png b/app/src/main/res/drawable-xhdpi/ic_stat_paused.png new file mode 100644 index 000000000..834a83c8d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_paused.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_suggestion.png b/app/src/main/res/drawable-xhdpi/ic_stat_suggestion.png new file mode 100644 index 000000000..e85724bb9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_suggestion.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_paused.png b/app/src/main/res/drawable-xxhdpi/ic_stat_paused.png new file mode 100644 index 000000000..883a124a3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_paused.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_suggestion.png b/app/src/main/res/drawable-xxhdpi/ic_stat_suggestion.png new file mode 100644 index 000000000..17f64644a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_suggestion.png differ 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/fading_snackbar_background.xml b/app/src/main/res/drawable/fading_snackbar_background.xml deleted file mode 100644 index b439322e0..000000000 --- a/app/src/main/res/drawable/fading_snackbar_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_pause.xml b/app/src/main/res/drawable/ic_action_pause.xml new file mode 100644 index 000000000..147cc6322 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_pause.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_action_resume.xml b/app/src/main/res/drawable/ic_action_resume.xml new file mode 100644 index 000000000..9811d4077 --- /dev/null +++ b/app/src/main/res/drawable/ic_action_resume.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_book_cross.xml b/app/src/main/res/drawable/ic_book_cross.xml deleted file mode 100644 index 6dc368b79..000000000 --- a/app/src/main/res/drawable/ic_book_cross.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_book_search.xml b/app/src/main/res/drawable/ic_book_search.xml deleted file mode 100644 index 50f9dec7a..000000000 --- a/app/src/main/res/drawable/ic_book_search.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_cancel_multiple.xml b/app/src/main/res/drawable/ic_cancel_multiple.xml new file mode 100644 index 000000000..44aa35c5b --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_multiple.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_favicon_fallback.xml b/app/src/main/res/drawable/ic_favicon_fallback.xml deleted file mode 100644 index 24996b554..000000000 --- a/app/src/main/res/drawable/ic_favicon_fallback.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hidden.xml b/app/src/main/res/drawable/ic_hidden.xml deleted file mode 100644 index 82816e502..000000000 --- a/app/src/main/res/drawable/ic_hidden.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loading.xml b/app/src/main/res/drawable/ic_loading.xml deleted file mode 100644 index 901cbecd1..000000000 --- a/app/src/main/res/drawable/ic_loading.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_local_library.xml b/app/src/main/res/drawable/ic_local_library.xml deleted file mode 100644 index f7b156e43..000000000 --- a/app/src/main/res/drawable/ic_local_library.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_restart_black.xml b/app/src/main/res/drawable/ic_restart_black.xml deleted file mode 100644 index afa22f223..000000000 --- a/app/src/main/res/drawable/ic_restart_black.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shown.xml b/app/src/main/res/drawable/ic_shown.xml deleted file mode 100644 index ee4887a82..000000000 --- a/app/src/main/res/drawable/ic_shown.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shown_hidden.xml b/app/src/main/res/drawable/ic_shown_hidden.xml deleted file mode 100644 index 5405a4747..000000000 --- a/app/src/main/res/drawable/ic_shown_hidden.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml deleted file mode 100644 index d2b46cc1c..000000000 --- a/app/src/main/res/drawable/ic_star.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_star_manga_info.xml b/app/src/main/res/drawable/ic_star_manga_info.xml deleted file mode 100644 index 803311090..000000000 --- a/app/src/main/res/drawable/ic_star_manga_info.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_totoro.xml b/app/src/main/res/drawable/ic_totoro.xml deleted file mode 100644 index fba48661f..000000000 --- a/app/src/main/res/drawable/ic_totoro.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/tab_rounded_rectangle.xml b/app/src/main/res/drawable/tab_rounded_rectangle.xml deleted file mode 100644 index 78b0e7813..000000000 --- a/app/src/main/res/drawable/tab_rounded_rectangle.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selector_drawable.xml b/app/src/main/res/drawable/tab_selector_drawable.xml deleted file mode 100644 index 7748f1d8f..000000000 --- a/app/src/main/res/drawable/tab_selector_drawable.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/activity_categories.xml b/app/src/main/res/layout-w600dp/activity_categories.xml index fbd53ee8b..a7c07c39e 100644 --- a/app/src/main/res/layout-w600dp/activity_categories.xml +++ b/app/src/main/res/layout-w600dp/activity_categories.xml @@ -64,7 +64,7 @@ app:icon="@drawable/ic_add" app:layout_anchor="@id/recyclerView" app:layout_anchorGravity="bottom|end" - app:layout_behavior="org.koitharu.kotatsu.base.ui.util.ShrinkOnScrollBehavior" + app:layout_behavior="org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior" app:layout_dodgeInsetEdges="bottom" /> diff --git a/app/src/main/res/layout-w600dp/activity_main.xml b/app/src/main/res/layout-w600dp/activity_main.xml index 6e09a349a..320e64005 100644 --- a/app/src/main/res/layout-w600dp/activity_main.xml +++ b/app/src/main/res/layout-w600dp/activity_main.xml @@ -40,7 +40,7 @@ android:stateListAnimator="@null" app:liftOnScroll="false"> - - - - - diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 13d34ca0d..11ca45679 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -48,7 +48,7 @@ app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet" tools:visibility="visible"> - - + - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ebf06ab36..d3d46a580 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -24,7 +24,7 @@ android:stateListAnimator="@null" app:liftOnScroll="false"> - - diff --git a/app/src/main/res/layout/activity_search_multi.xml b/app/src/main/res/layout/activity_search_multi.xml index 783cb6d61..7d741d592 100644 --- a/app/src/main/res/layout/activity_search_multi.xml +++ b/app/src/main/res/layout/activity_search_multi.xml @@ -13,7 +13,7 @@ app:elevation="0dp" app:liftOnScroll="false"> - - - + - + + + + + android:layout_height="wrap_content" + android:autofillHints="emailAddress" + android:imeOptions="actionDone" + android:inputType="textEmailAddress" + android:singleLine="true" + android:textSize="16sp" + tools:text="test@mail.com" /> - + - +