From 1cbb82589220417d8d6684bf641e3c2864b02fb7 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 10 May 2022 15:09:27 +0300 Subject: [PATCH 01/64] Bookmarks feature --- app/build.gradle | 16 +- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 2 + .../AdapterDelegateClickListenerAdapter.kt | 20 + .../kotatsu/bookmarks/BookmarksModule.kt | 10 + .../kotatsu/bookmarks/data/BookmarkEntity.kt | 28 ++ .../bookmarks/data/BookmarkWithManga.kt | 23 + .../kotatsu/bookmarks/data/BookmarksDao.kt | 26 + .../kotatsu/bookmarks/data/EntityMapping.kt | 31 ++ .../kotatsu/bookmarks/domain/Bookmark.kt | 43 ++ .../bookmarks/domain/BookmarksRepository.kt | 38 ++ .../kotatsu/bookmarks/ui/BookmarkListAD.kt | 51 ++ .../kotatsu/bookmarks/ui/BookmarksAdapter.kt | 30 ++ .../koitharu/kotatsu/core/db/MangaDatabase.kt | 9 +- .../core/db/migrations/Migration10To11.kt | 26 + .../koitharu/kotatsu/details/DetailsModule.kt | 2 +- .../kotatsu/details/ui/DetailsActivity.kt | 3 + .../kotatsu/details/ui/DetailsFragment.kt | 41 +- .../kotatsu/details/ui/DetailsViewModel.kt | 17 + .../details/ui/adapter/ChapterListItemAD.kt | 8 +- .../list/ui/filter/FilterBottomSheet.kt | 10 +- .../koitharu/kotatsu/reader/ReaderModule.kt | 1 + .../kotatsu/reader/ui/ReaderActivity.kt | 29 +- .../koitharu/kotatsu/reader/ui/ReaderState.kt | 29 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 66 ++- .../kotatsu/reader/ui/pager/ReaderPage.kt | 27 +- app/src/main/res/drawable/ic_bookmark.xml | 12 + .../main/res/drawable/ic_bookmark_added.xml | 12 + .../res/layout-w600dp/fragment_details.xml | 41 +- app/src/main/res/layout/fragment_details.xml | 41 +- app/src/main/res/layout/item_bookmark.xml | 19 + app/src/main/res/menu/opt_reader_bottom.xml | 14 +- app/src/main/res/menu/popup_bookmark.xml | 9 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 469 +++++++++--------- 34 files changed, 905 insertions(+), 300 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt create mode 100644 app/src/main/res/drawable/ic_bookmark.xml create mode 100644 app/src/main/res/drawable/ic_bookmark_added.xml create mode 100644 app/src/main/res/layout/item_bookmark.xml create mode 100644 app/src/main/res/menu/popup_bookmark.xml diff --git a/app/build.gradle b/app/build.gradle index 913934c49..1c67c45f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,15 +49,15 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() freeCompilerArgs += [ - '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', - '-opt-in=kotlinx.coroutines.FlowPreview', - '-opt-in=kotlin.contracts.ExperimentalContracts', - '-opt-in=coil.annotation.ExperimentalCoilApi', + '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', + '-opt-in=kotlinx.coroutines.FlowPreview', + '-opt-in=kotlin.contracts.ExperimentalContracts', + '-opt-in=coil.annotation.ExperimentalCoilApi', ] } lint { abortOnError false - disable 'MissingTranslation', 'PrivateResource' + disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged' } testOptions { unitTests.includeAndroidResources = true @@ -96,12 +96,12 @@ dependencies { kapt 'androidx.room:room-compiler:2.4.2' implementation 'com.squareup.okhttp3:okhttp:4.9.3' - implementation 'com.squareup.okio:okio:3.0.0' + implementation 'com.squareup.okio:okio:3.1.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' - implementation 'io.insert-koin:koin-android:3.1.6' + implementation 'io.insert-koin:koin-android:3.2.0' implementation 'io.coil-kt:coil-base:2.0.0-rc03' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' @@ -110,7 +110,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' - testImplementation 'io.insert-koin:koin-test-junit4:3.1.5' + testImplementation 'io.insert-koin:koin-test-junit4:3.2.0' androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 7e755507a..4ae7a438f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.strictmode.FragmentStrictMode import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.koitharu.kotatsu.bookmarks.bookmarksModule import org.koitharu.kotatsu.core.db.databaseModule import org.koitharu.kotatsu.core.github.githubModule import org.koitharu.kotatsu.core.network.networkModule @@ -67,6 +68,7 @@ class KotatsuApp : Application() { readerModule, appWidgetModule, suggestionsModule, + bookmarksModule, ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt new file mode 100644 index 000000000..650e816c5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.base.ui.list + +import android.view.View +import android.view.View.OnClickListener +import android.view.View.OnLongClickListener +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder + +class AdapterDelegateClickListenerAdapter( + private val adapterDelegate: AdapterDelegateViewBindingViewHolder, + private val clickListener: OnListItemClickListener, +) : OnClickListener, OnLongClickListener { + + override fun onClick(v: View) { + clickListener.onItemClick(adapterDelegate.item, v) + } + + override fun onLongClick(v: View): Boolean { + return clickListener.onItemLongClick(adapterDelegate.item, v) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt new file mode 100644 index 000000000..4a8294765 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/BookmarksModule.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.bookmarks + +import org.koin.dsl.module +import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository + +val bookmarksModule + get() = module { + + factory { BookmarksRepository(get()) } + } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt new file mode 100644 index 000000000..0959b3362 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.bookmarks.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import org.koitharu.kotatsu.core.db.entity.MangaEntity + +@Entity( + tableName = "bookmarks", + primaryKeys = ["manga_id", "page_id"], + foreignKeys = [ + ForeignKey( + entity = MangaEntity::class, + parentColumns = ["manga_id"], + childColumns = ["manga_id"], + onDelete = ForeignKey.CASCADE + ), + ] +) +class BookmarkEntity( + @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, + @ColumnInfo(name = "page_id", index = true) val pageId: Long, + @ColumnInfo(name = "chapter_id") val chapterId: Long, + @ColumnInfo(name = "page") val page: Int, + @ColumnInfo(name = "scroll") val scroll: Int, + @ColumnInfo(name = "image") val imageUrl: String, + @ColumnInfo(name = "created_at") val createdAt: Long, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt new file mode 100644 index 000000000..4bd63d65d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkWithManga.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.bookmarks.data + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity + +class BookmarkWithManga( + @Embedded val bookmark: BookmarkEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "manga_id" + ) + val manga: MangaEntity, + @Relation( + parentColumn = "manga_id", + entityColumn = "tag_id", + associateBy = Junction(MangaTagsEntity::class) + ) + val tags: List, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt new file mode 100644 index 000000000..dd023be7a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.bookmarks.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class BookmarksDao { + + @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page") + abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow + + @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC") + abstract fun observe(mangaId: Long): Flow> + + @Insert + abstract suspend fun insert(entity: BookmarkEntity) + + @Delete + abstract suspend fun delete(entity: BookmarkEntity) + + @Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId") + abstract suspend fun delete(mangaId: Long, pageId: Long) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt new file mode 100644 index 000000000..981aa05ea --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.bookmarks.data + +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.parsers.model.Manga +import java.util.* + +fun BookmarkWithManga.toBookmark() = bookmark.toBookmark( + manga.toManga(tags.toMangaTags()) +) + +fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark( + manga = manga, + pageId = pageId, + chapterId = chapterId, + page = page, + scroll = scroll, + imageUrl = imageUrl, + createdAt = Date(createdAt), +) + +fun Bookmark.toEntity() = BookmarkEntity( + mangaId = manga.id, + pageId = pageId, + chapterId = chapterId, + page = page, + scroll = scroll, + imageUrl = imageUrl, + createdAt = createdAt.time, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt new file mode 100644 index 000000000..0b76c6537 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt @@ -0,0 +1,43 @@ +package org.koitharu.kotatsu.bookmarks.domain + +import org.koitharu.kotatsu.parsers.model.Manga +import java.util.* + +class Bookmark( + val manga: Manga, + val pageId: Long, + val chapterId: Long, + val page: Int, + val scroll: Int, + val imageUrl: String, + val createdAt: Date, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Bookmark + + if (manga != other.manga) return false + if (pageId != other.pageId) return false + if (chapterId != other.chapterId) return false + if (page != other.page) return false + if (scroll != other.scroll) return false + if (imageUrl != other.imageUrl) return false + if (createdAt != other.createdAt) return false + + return true + } + + override fun hashCode(): Int { + var result = manga.hashCode() + result = 31 * result + pageId.hashCode() + result = 31 * result + chapterId.hashCode() + result = 31 * result + page + result = 31 * result + scroll + result = 31 * result + imageUrl.hashCode() + result = 31 * result + createdAt.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt new file mode 100644 index 000000000..df63c03aa --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.bookmarks.domain + +import androidx.room.withTransaction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koitharu.kotatsu.bookmarks.data.toBookmark +import org.koitharu.kotatsu.bookmarks.data.toEntity +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.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.mapItems + +class BookmarksRepository( + private val db: MangaDatabase, +) { + + fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow { + return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) } + } + + fun observeBookmarks(manga: Manga): Flow> { + return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) } + } + + suspend fun addBookmark(bookmark: Bookmark) { + db.withTransaction { + val tags = bookmark.manga.tags.toEntities() + db.tagsDao.upsert(tags) + db.mangaDao.upsert(bookmark.manga.toEntity(), tags) + db.bookmarksDao.insert(bookmark.toEntity()) + } + } + + suspend fun removeBookmark(mangaId: Long, pageId: Long) { + db.bookmarksDao.delete(mangaId, pageId) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt new file mode 100644 index 000000000..f8aa0e638 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarkListAD.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.bookmarks.ui + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.request.Disposable +import coil.size.Scale +import coil.util.CoilUtils +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.databinding.ItemBookmarkBinding +import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.newImageRequest +import org.koitharu.kotatsu.utils.ext.referer + +fun bookmarkListAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + clickListener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) } +) { + + var imageRequest: Disposable? = null + val listener = AdapterDelegateClickListenerAdapter(this, clickListener) + + binding.root.setOnClickListener(listener) + binding.root.setOnLongClickListener(listener) + + bind { + imageRequest?.dispose() + imageRequest = binding.imageViewThumb.newImageRequest(item.imageUrl) + .referer(item.manga.publicUrl) + .placeholder(R.drawable.ic_placeholder) + .fallback(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + .allowRgb565(true) + .scale(Scale.FILL) + .lifecycle(lifecycleOwner) + .enqueueWith(coil) + } + + onViewRecycled { + imageRequest?.dispose() + imageRequest = null + CoilUtils.dispose(binding.imageViewThumb) + binding.imageViewThumb.setImageDrawable(null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt new file mode 100644 index 000000000..92040bc97 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/ui/BookmarksAdapter.kt @@ -0,0 +1,30 @@ +package org.koitharu.kotatsu.bookmarks.ui + +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 + +class BookmarksAdapter( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + clickListener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter( + DiffCallback(), + bookmarkListAD(coil, lifecycleOwner, clickListener) +) { + + private class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { + return oldItem.manga.id == newItem.manga.id && oldItem.chapterId == newItem.chapterId + } + + override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { + return oldItem.imageUrl == newItem.imageUrl + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 436455014..0714c0fcc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity +import org.koitharu.kotatsu.bookmarks.data.BookmarksDao import org.koitharu.kotatsu.core.db.dao.* import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.migrations.* @@ -20,9 +22,9 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity entities = [ MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, - TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class + TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ], - version = 10 + version = 11, ) abstract class MangaDatabase : RoomDatabase() { @@ -43,6 +45,8 @@ abstract class MangaDatabase : RoomDatabase() { abstract val trackLogsDao: TrackLogsDao abstract val suggestionDao: SuggestionDao + + abstract val bookmarksDao: BookmarksDao } fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( @@ -59,6 +63,7 @@ fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( Migration7To8(), Migration8To9(), Migration9To10(), + Migration10To11(), ).addCallback( DatabasePrePopulateCallback(context.resources) ).build() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt new file mode 100644 index 000000000..5d80708fe --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration10To11.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration10To11 : Migration(10, 11) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `bookmarks` ( + `manga_id` INTEGER NOT NULL, + `page_id` INTEGER NOT NULL, + `chapter_id` INTEGER NOT NULL, + `page` INTEGER NOT NULL, + `scroll` INTEGER NOT NULL, + `image` TEXT NOT NULL, + `created_at` INTEGER NOT NULL, + PRIMARY KEY(`manga_id`, `page_id`), + FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE ) + """.trimIndent() + ) + database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)") + database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt index 7e3bd8622..916b75de1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/DetailsModule.kt @@ -8,6 +8,6 @@ val detailsModule get() = module { viewModel { intent -> - DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get()) + DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index a1920bf80..f6ecc6f0d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -83,6 +83,9 @@ class DetailsActivity : viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onError.observe(this, ::onError) + viewModel.onShowToast.observe(this) { + binding.snackbar.show(messageText = getString(it), longDuration = false) + } registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 817909268..51d320e00 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.net.toUri import androidx.core.text.parseAsHtml +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding import coil.ImageLoader @@ -21,7 +22,11 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel 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.BookmarksAdapter import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet @@ -41,7 +46,8 @@ class DetailsFragment : BaseFragment(), View.OnClickListener, View.OnLongClickListener, - ChipsView.OnChipClickListener { + ChipsView.OnChipClickListener, + OnListItemClickListener { private val viewModel by sharedViewModel() private val coil by inject(mode = LazyThreadSafetyMode.NONE) @@ -69,6 +75,7 @@ class DetailsFragment : viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged) + viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -76,6 +83,24 @@ class DetailsFragment : inflater.inflate(R.menu.opt_details_info, menu) } + override fun onItemClick(item: Bookmark, view: View) { + val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.measuredWidth, view.measuredHeight) + startActivity(ReaderActivity.newIntent(view.context, item), options.toBundle()) + } + + override fun onItemLongClick(item: Bookmark, view: View): Boolean { + val menu = PopupMenu(view.context, view) + menu.inflate(R.menu.popup_bookmark) + menu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_remove -> viewModel.removeBookmark(item) + } + true + } + menu.show() + return true + } + private fun onMangaUpdated(manga: Manga) { with(binding) { // Main @@ -176,6 +201,20 @@ class DetailsFragment : } } + private fun onBookmarksChanged(bookmarks: List) { + var adapter = binding.recyclerViewBookmarks.adapter as? BookmarksAdapter + binding.groupBookmarks.isGone = bookmarks.isEmpty() + if (adapter != null) { + adapter.items = bookmarks + } else { + adapter = BookmarksAdapter(coil, viewLifecycleOwner, this) + adapter.items = bookmarks + binding.recyclerViewBookmarks.adapter = adapter + val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing) + binding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing)) + } + } + override fun onClick(v: View) { val manga = viewModel.manga.value ?: return when (v.id) { 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 index 0ee363efd..c1624a6b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -10,9 +10,12 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.plus import org.koitharu.kotatsu.BuildConfig +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.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings @@ -39,6 +42,7 @@ class DetailsViewModel( private val localMangaRepository: LocalMangaRepository, private val trackingRepository: TrackingRepository, private val mangaDataRepository: MangaDataRepository, + private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, ) : BaseViewModel() { @@ -46,6 +50,8 @@ class DetailsViewModel( private val mangaData = MutableStateFlow(intent.manga) private val selectedBranch = MutableStateFlow(null) + val onShowToast = SingleLiveEvent() + private val history = mangaData.mapNotNull { it?.id } .distinctUntilChanged() .flatMapLatest { mangaId -> @@ -85,6 +91,10 @@ class DetailsViewModel( val isChaptersReversed = chaptersReversed .asLiveData(viewModelScope.coroutineContext) + val bookmarks = mangaData.flatMapLatest { + if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val onMangaRemoved = SingleLiveEvent() val branches = mangaData.map { @@ -149,6 +159,13 @@ class DetailsViewModel( } } + 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 } 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 index 9a423b2e2..f65951d47 100644 --- 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 @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.details.ui.adapter -import android.view.View 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 @@ -21,11 +21,7 @@ fun chapterListItemAD( { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) } ) { - val eventListener = object : View.OnClickListener, View.OnLongClickListener { - override fun onClick(v: View) = clickListener.onItemClick(item, v) - override fun onLongClick(v: View) = clickListener.onItemLongClick(item, v) - } - + val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener) itemView.setOnClickListener(eventListener) itemView.setOnLongClickListener(eventListener) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt index 81c79e1ae..7583b2e8c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterBottomSheet.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.* import androidx.appcompat.widget.SearchView import androidx.fragment.app.FragmentManager -import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet @@ -14,11 +13,14 @@ import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel import org.koitharu.kotatsu.utils.BottomSheetToolbarController -class FilterBottomSheet : BaseBottomSheet(), MenuItem.OnActionExpandListener, - SearchView.OnQueryTextListener, DialogInterface.OnKeyListener { +class FilterBottomSheet : + BaseBottomSheet(), + MenuItem.OnActionExpandListener, + SearchView.OnQueryTextListener, + DialogInterface.OnKeyListener { private val viewModel by sharedViewModel( - owner = { from(requireParentFragment(), requireParentFragment()) } + owner = { requireParentFragment() } ) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index c83ad608b..a27fb9e8d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -26,6 +26,7 @@ val readerModule shortcutsRepository = get(), settings = get(), pageSaveHelper = get(), + bookmarksRepository = get(), ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index ca9228207..c54ed94d6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -29,6 +29,7 @@ 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.ExceptionResolver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.prefs.ReaderMode @@ -103,6 +104,12 @@ class ReaderActivity : onLoadingStateChanged(viewModel.isLoading.value == true) } viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure) + viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged) + viewModel.onShowToast.observe(this) { msgId -> + Snackbar.make(binding.container, msgId, Snackbar.LENGTH_SHORT) + .setAnchorView(binding.appbarBottom) + .show() + } } private fun onInitReader(mode: ReaderMode) { @@ -189,6 +196,13 @@ class ReaderActivity : viewModel.saveCurrentPage(page, savePageRequest) } ?: showWaitWhileLoading() } + R.id.action_bookmark -> { + if (viewModel.isBookmarkAdded.value == true) { + viewModel.removeBookmark() + } else { + viewModel.addBookmark() + } + } else -> return super.onOptionsItemSelected(item) } return true @@ -309,8 +323,8 @@ class ReaderActivity : val transition = TransitionSet() .setOrdering(TransitionSet.ORDERING_TOGETHER) .addTransition(Slide(Gravity.TOP).addTarget(binding.appbarTop)) - binding.appbarBottom?.let { botomBar -> - transition.addTransition(Slide(Gravity.BOTTOM).addTarget(botomBar)) + binding.appbarBottom?.let { bottomBar -> + transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar)) } TransitionManager.beginDelayedTransition(binding.root, transition) binding.appbarTop.isVisible = isUiVisible @@ -351,6 +365,12 @@ class ReaderActivity : setUiIsVisible(!binding.appbarTop.isVisible) } + private fun onBookmarkStateChanged(isAdded: Boolean) { + val menuItem = binding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return + menuItem.setTitle(if (isAdded) R.string.bookmark_remove else R.string.bookmark_add) + menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark) + } + private fun onUiStateChanged(uiState: ReaderUiState, previous: ReaderUiState?) { title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_) supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) { @@ -419,6 +439,11 @@ class ReaderActivity : .putExtra(EXTRA_STATE, state) } + fun newIntent(context: Context, bookmark: Bookmark): Intent { + val state = ReaderState(bookmark.chapterId, bookmark.page, bookmark.scroll) + return newIntent(context, bookmark.manga, state) + } + fun newIntent(context: Context, mangaId: Long): Intent { return Intent(context, ReaderActivity::class.java) .putExtra(MangaIntent.KEY_ID, mangaId) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt index 0fe72b499..2eb7cc960 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderState.kt @@ -9,23 +9,20 @@ import org.koitharu.kotatsu.parsers.model.Manga data class ReaderState( val chapterId: Long, val page: Int, - val scroll: Int + val scroll: Int, ) : Parcelable { - companion object { + constructor(history: MangaHistory) : this( + chapterId = history.chapterId, + page = history.page, + scroll = history.scroll, + ) - fun from(history: MangaHistory) = ReaderState( - chapterId = history.chapterId, - page = history.page, - scroll = history.scroll - ) - - fun initial(manga: Manga, branch: String?) = ReaderState( - chapterId = manga.chapters?.firstOrNull { - it.branch == branch - }?.id ?: error("Cannot find first chapter"), - page = 0, - scroll = 0 - ) - } + constructor(manga: Manga, branch: String?) : this( + chapterId = manga.chapters?.firstOrNull { + it.branch == branch + }?.id ?: error("Cannot find first chapter"), + page = 0, + scroll = 0, + ) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index bfe5ac663..f9ba0655f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui import android.net.Uri import android.util.LongSparseArray import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* @@ -10,10 +11,13 @@ import kotlinx.coroutines.flow.* import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaUtils 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.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.parser.MangaRepository @@ -31,6 +35,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import java.util.* class ReaderViewModel( private val intent: MangaIntent, @@ -39,12 +44,14 @@ class ReaderViewModel( private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, private val shortcutsRepository: ShortcutsRepository, + private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, private val pageSaveHelper: PageSaveHelper, ) : BaseViewModel() { private var loadingJob: Job? = null private var pageSaveJob: Job? = null + private var bookmarkJob: Job? = null private val currentState = MutableStateFlow(initialState) private val mangaData = MutableStateFlow(intent.manga) private val chapters = LongSparseArray() @@ -53,6 +60,7 @@ class ReaderViewModel( val readerMode = MutableLiveData() val onPageSaved = SingleLiveEvent() + val onShowToast = SingleLiveEvent() val uiState = combine( mangaData, currentState, @@ -89,6 +97,16 @@ class ReaderViewModel( val onZoomChanged = SingleLiveEvent() + val isBookmarkAdded: LiveData = currentState.flatMapLatest { state -> + val manga = mangaData.value + if (state == null || manga == null) { + flowOf(false) + } else { + bookmarksRepository.observeBookmark(manga, state.chapterId, state.page) + .map { it != null } + } + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + init { loadImpl() subscribeToSettings() @@ -187,10 +205,9 @@ class ReaderViewModel( fun onCurrentPageChanged(position: Int) { val pages = content.value?.pages ?: return - pages.getOrNull(position)?.let { - val currentValue = currentState.value - if (currentValue != null && currentValue.chapterId != it.chapterId) { - currentState.value = currentValue.copy(chapterId = it.chapterId) + pages.getOrNull(position)?.let { page -> + currentState.update { cs -> + cs?.copy(chapterId = page.chapterId, page = page.index) } } if (pages.isEmpty() || loadingJob?.isActive == true) { @@ -207,6 +224,41 @@ class ReaderViewModel( } } + fun addBookmark() { + if (bookmarkJob?.isActive == true) { + return + } + bookmarkJob = launchJob { + loadingJob?.join() + val state = checkNotNull(currentState.value) + val page = checkNotNull(getCurrentPage()) { "Page not found" } + val bookmark = Bookmark( + manga = checkNotNull(mangaData.value), + pageId = page.id, + chapterId = state.chapterId, + page = state.page, + scroll = state.scroll, + imageUrl = page.preview ?: pageLoader.getPageUrl(page), + createdAt = Date(), + ) + bookmarksRepository.addBookmark(bookmark) + onShowToast.call(R.string.bookmark_added) + } + } + + fun removeBookmark() { + if (bookmarkJob?.isActive == true) { + return + } + bookmarkJob = launchJob { + loadingJob?.join() + val manga = checkNotNull(mangaData.value) + val page = checkNotNull(getCurrentPage()) { "Page not found" } + bookmarksRepository.removeBookmark(manga.id, page.id) + onShowToast.call(R.string.bookmark_removed) + } + } + private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") @@ -229,8 +281,8 @@ class ReaderViewModel( // obtain state if (currentState.value == null) { currentState.value = historyRepository.getOne(manga)?.let { - ReaderState.from(it) - } ?: ReaderState.initial(manga, preselectedBranch) + ReaderState(it) + } ?: ReaderState(manga, preselectedBranch) } val branch = chapters[currentState.value?.chapterId ?: 0L].branch @@ -259,7 +311,7 @@ class ReaderViewModel( val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } val repo = MangaRepository(manga.source) return repo.getPages(chapter).mapIndexed { index, page -> - ReaderPage.from(page, index, chapterId) + ReaderPage(page, index, chapterId) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt index bc3f3220e..6773f5dc3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/ReaderPage.kt @@ -13,27 +13,24 @@ data class ReaderPage( val preview: String?, val chapterId: Long, val index: Int, - val source: MangaSource + val source: MangaSource, ) : Parcelable { + constructor(page: MangaPage, index: Int, chapterId: Long) : this( + id = page.id, + url = page.url, + referer = page.referer, + preview = page.preview, + chapterId = chapterId, + index = index, + source = page.source, + ) + fun toMangaPage() = MangaPage( id = id, url = url, referer = referer, preview = preview, - source = source + source = source, ) - - companion object { - - fun from(page: MangaPage, index: Int, chapterId: Long) = ReaderPage( - id = page.id, - url = page.url, - referer = page.referer, - preview = page.preview, - chapterId = chapterId, - index = index, - source = page.source - ) - } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 000000000..f5457e1bd --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bookmark_added.xml b/app/src/main/res/drawable/ic_bookmark_added.xml new file mode 100644 index 000000000..9ebb3889b --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_added.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml index 936a0ac6c..6a862073a 100644 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ b/app/src/main/res/layout-w600dp/fragment_details.xml @@ -157,6 +157,37 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/layout_titles" /> + + + + @@ -189,6 +220,14 @@ app:showAnimationBehavior="inward" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index e001bb43d..148179075 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -161,6 +161,37 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_read" /> + + + + @@ -193,6 +224,14 @@ app:showAnimationBehavior="inward" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookmark.xml b/app/src/main/res/layout/item_bookmark.xml new file mode 100644 index 000000000..78aab3400 --- /dev/null +++ b/app/src/main/res/layout/item_bookmark.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml index b56029f73..978a29d74 100644 --- a/app/src/main/res/menu/opt_reader_bottom.xml +++ b/app/src/main/res/menu/opt_reader_bottom.xml @@ -6,10 +6,9 @@ tools:ignore="AlwaysShowAction"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 282d15034..20debe81b 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -13,6 +13,8 @@ 2dp 86dp 120dp + 120dp + 4dp 62dp 120dp 48dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d13563fab..13dde4b4e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,241 +1,241 @@ Kotatsu - Close menu - Open menu - Local storage - Favourites - History - An error occurred - Could not connect to the Internet - Details - Chapters - List - Detailed list - Grid - List mode - Settings - Remote sources - Loading… - Computing… - Chapter %1$d of %2$d - Close - Try again - Clear history - Nothing found - No history yet - Read - No favourites yet - Favourite this - New category - Add - Enter category name - Save - Share - Create shortcut… - Share %s - Search - Search manga - Downloading… - Processing… - Downloaded - Downloads - Name - Popular - Updated - Newest - Rating - Sorting order - Filter - Theme - Light - Dark - Follow system - Pages - Clear - Clear all reading history permanently? - Remove - \"%s\" removed from history - \"%s\" deleted from local storage - Wait for loading to finish… - Save page - Saved - Share image - Import - Delete - This operation is not supported - Either pick a ZIP or CBZ file. - No description - History and cache - Clear page cache - Cache - B|kB|MB|GB|TB - Standard - Webtoon - Read mode - Grid size - Search on %s - Delete manga - Delete \"%s\" from device permanently? - Reader settings - Switch pages - Edge taps - Volume buttons - Continue - Warning - This may transfer a lot of data - Don\'t ask again - Cancelling… - Error - Clear thumbnails cache - Clear search history - Cleared - Gestures only - Internal storage - External storage - Domain - Check for new versions of the app - A new version of the app is available - Show notification if a new version is available - Open in web browser - This manga has %s. Save all of it? - Save - Notifications - %1$d of %2$d on - New chapters - Download - Read from start - Restart - Notifications settings - Notification sound - LED indicator - Vibration - Favourite categories - Categories… - Rename - Remove the \"%s\" category from your favourites? \nAll manga in it will be lost. - Remove - It\'s kind of empty here… - You can use categories to organize your favourites. Press «+» to create a category - Try to reformulate the query. - What you read will be displayed here - Find what to read in side menu. - Save something first - Save it from online sources or import files. - Shelf - Recent - Page animation - Folder for downloads - Not available - No available storage - Other storage + Close menu + Open menu + Local storage + Favourites + History + An error occurred + Could not connect to the Internet + Details + Chapters + List + Detailed list + Grid + List mode + Settings + Remote sources + Loading… + Computing… + Chapter %1$d of %2$d + Close + Try again + Clear history + Nothing found + No history yet + Read + No favourites yet + Favourite this + New category + Add + Enter category name + Save + Share + Create shortcut… + Share %s + Search + Search manga + Downloading… + Processing… + Downloaded + Downloads + Name + Popular + Updated + Newest + Rating + Sorting order + Filter + Theme + Light + Dark + Follow system + Pages + Clear + Clear all reading history permanently? + Remove + \"%s\" removed from history + \"%s\" deleted from local storage + Wait for loading to finish… + Save page + Saved + Share image + Import + Delete + This operation is not supported + Either pick a ZIP or CBZ file. + No description + History and cache + Clear page cache + Cache + B|kB|MB|GB|TB + Standard + Webtoon + Read mode + Grid size + Search on %s + Delete manga + Delete \"%s\" from device permanently? + Reader settings + Switch pages + Edge taps + Volume buttons + Continue + Warning + This may transfer a lot of data + Don\'t ask again + Cancelling… + Error + Clear thumbnails cache + Clear search history + Cleared + Gestures only + Internal storage + External storage + Domain + Check for new versions of the app + A new version of the app is available + Show notification if a new version is available + Open in web browser + This manga has %s. Save all of it? + Save + Notifications + %1$d of %2$d on + New chapters + Download + Read from start + Restart + Notifications settings + Notification sound + LED indicator + Vibration + Favourite categories + Categories… + Rename + Remove the \"%s\" category from your favourites? \nAll manga in it will be lost. + Remove + It\'s kind of empty here… + You can use categories to organize your favourites. Press «+» to create a category + Try to reformulate the query. + What you read will be displayed here + Find what to read in side menu. + Save something first + Save it from online sources or import files. + Shelf + Recent + Page animation + Folder for downloads + Not available + No available storage + Other storage Done - All favourites - Empty category - Read later - Updates - New chapters of what you are reading is shown here - Search results - Related - New version: %s - Size: %s - Waiting for network… - Clear updates feed - Cleared - Rotate screen - Update - Feed update will start soon - Look for updates - Don\'t check - Enter password - Wrong password - Protect the app - Ask for password when starting Kotatsu - Repeat the password - Mismatching passwords - About - Version %s - Check for updates - Checking for updates… - Could not look for updates - No updates available - Right-to-left (←) - Prefer right-to-left (←) reader - Reading mode can be set up separately for each series - New category - Scale mode - Fit center - Fit to height - Fit to width - Keep at start - Black - Uses less power on AMOLED screens - Restart required - Backup and restore - Create data backup - Restore from backup - Restored - Preparing… - Create issue on GitHub - File not found - All data was restored - The data was restored, but there are errors - You can create backup of your history and favourites and restore it - Just now - Yesterday - Long ago - Group - Today - Tap to try again - The chosen configuration will be remembered for this manga - Silent - CAPTCHA required - Solve - Clear cookies - All cookies were removed - Checking for new chapters: %1$d of %2$d - Clear feed - Clear all update history permanently? - Check for new chapters - Reverse - Sign in - Sign in to view this content - Default: %s - …and %1$d more - Next - Enter a password to start the app with - Confirm - The password must be 4 characters or more + All favourites + Empty category + Read later + Updates + New chapters of what you are reading is shown here + Search results + Related + New version: %s + Size: %s + Waiting for network… + Clear updates feed + Cleared + Rotate screen + Update + Feed update will start soon + Look for updates + Don\'t check + Enter password + Wrong password + Protect the app + Ask for password when starting Kotatsu + Repeat the password + Mismatching passwords + About + Version %s + Check for updates + Checking for updates… + Could not look for updates + No updates available + Right-to-left (←) + Prefer right-to-left (←) reader + Reading mode can be set up separately for each series + New category + Scale mode + Fit center + Fit to height + Fit to width + Keep at start + Black + Uses less power on AMOLED screens + Restart required + Backup and restore + Create data backup + Restore from backup + Restored + Preparing… + Create issue on GitHub + File not found + All data was restored + The data was restored, but there are errors + You can create backup of your history and favourites and restore it + Just now + Yesterday + Long ago + Group + Today + Tap to try again + The chosen configuration will be remembered for this manga + Silent + CAPTCHA required + Solve + Clear cookies + All cookies were removed + Checking for new chapters: %1$d of %2$d + Clear feed + Clear all update history permanently? + Check for new chapters + Reverse + Sign in + Sign in to view this content + Default: %s + …and %1$d more + Next + Enter a password to start the app with + Confirm + The password must be 4 characters or more Search only on %s - Remove all recent search queries permanently? - Other - Welcome - Backup saved - Some devices have different system behavior, which may break background tasks. - Read more - Queued - No active downloads - Download or read this missing chapter online. - The chapter is missing - Translate this app - Translation - Feedback - Topic on 4PDA + Remove all recent search queries permanently? + Other + Welcome + Backup saved + Some devices have different system behavior, which may break background tasks. + Read more + Queued + No active downloads + Download or read this missing chapter online. + The chapter is missing + Translate this app + Translation + Feedback + Topic on 4PDA Authorized - Logging in on %s is not supported - You will be logged out from all sources - Genres - Finished - Ongoing - Date format - Default - Exclude NSFW manga from history - You must enter a name - Numbered pages - Used sources - Available sources - Dynamic theme - Applies a theme created on the color scheme of your wallpaper + Logging in on %s is not supported + You will be logged out from all sources + Genres + Finished + Ongoing + Date format + Default + Exclude NSFW manga from history + You must enter a name + Numbered pages + Used sources + Available sources + Dynamic theme + Applies a theme created on the color scheme of your wallpaper Importing manga: %1$d of %2$d Screenshot policy Allow @@ -288,4 +288,9 @@ Edit Edit category No favourite categories + Add bookmark + Remove bookmark + Bookmarks + Bookmark removed + Bookmark added \ No newline at end of file From 3e785a2555e4ce1cef9becab01305e93b12dc009 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 10 May 2022 17:45:11 +0300 Subject: [PATCH 02/64] Refactor and optimization --- .idea/inspectionProfiles/Project_Default.xml | 3 + .../koitharu/kotatsu/utils/ext/DebugExt.kt | 3 + .../kotatsu/base/domain/MangaUtils.kt | 14 +- .../kotatsu/base/ui/BaseFullscreenActivity.kt | 8 +- .../koitharu/kotatsu/base/ui/BaseViewModel.kt | 6 +- .../kotatsu/core/github/GithubModule.kt | 4 +- .../koitharu/kotatsu/core/github/VersionId.kt | 40 ++- .../kotatsu/core/prefs/AppSettings.kt | 1 - .../kotatsu/core/prefs/AppSettingsObserver.kt | 35 +++ .../koitharu/kotatsu/core/prefs/ReaderMode.kt | 2 +- .../kotatsu/core/ui/AppCrashHandler.kt | 3 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 +- .../kotatsu/details/ui/DetailsViewModel.kt | 257 ++++-------------- .../details/ui/MangaDetailsDelegate.kt | 184 +++++++++++++ .../download/domain/DownloadManager.kt | 6 +- .../kotatsu/download/ui/DownloadsActivity.kt | 13 +- .../FavouritesCategoriesViewModel.kt | 11 +- .../ui/categories/adapter/CategoryAD.kt | 2 +- .../history/ui/HistoryListViewModel.kt | 15 +- .../kotatsu/list/ui/MangaListViewModel.kt | 25 +- .../list/ui/adapter/CurrentFilterAD.kt | 2 +- .../list/ui/filter/FilterCoordinator.kt | 10 +- .../kotatsu/local/ui/LocalListFragment.kt | 6 +- .../kotatsu/local/ui/LocalListViewModel.kt | 8 +- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 11 +- .../kotatsu/reader/data/ModelMapping.kt | 27 ++ .../kotatsu/reader/ui/ReaderActivity.kt | 100 +++---- .../reader/ui/ReaderControlDelegate.kt | 28 +- .../kotatsu/reader/ui/ReaderManager.kt | 45 +++ .../kotatsu/reader/ui/ReaderToastView.kt | 26 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 96 +++---- .../remotelist/ui/RemoteListViewModel.kt | 8 +- .../kotatsu/settings/AppUpdateChecker.kt | 33 +-- .../settings/SourceSettingsFragment.kt | 6 +- .../settings/backup/BackupSettingsFragment.kt | 9 +- .../utils/LifecycleAwareServiceConnection.kt | 28 +- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 11 + .../kotatsu/utils/ext/CoroutineExt.kt | 9 - .../koitharu/kotatsu/utils/ext/LiveDataExt.kt | 1 + app/src/main/res/menu/opt_reader_bottom.xml | 4 + .../koitharu/kotatsu/utils/ext/DebugExt.kt | 5 + 41 files changed, 591 insertions(+), 516 deletions(-) create mode 100644 app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt create mode 100644 app/src/release/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a6fb1fbe4..2bcd23609 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -4,6 +4,9 @@ + + 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 new file mode 100644 index 000000000..e00bb6a83 --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.utils.ext + +fun Throwable.printStackTraceDebug() = printStackTrace() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt index 03b0dd53b..6c481e467 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt @@ -3,21 +3,21 @@ package org.koitharu.kotatsu.base.domain import android.graphics.BitmapFactory import android.net.Uri import android.util.Size +import java.io.File +import java.io.InputStream +import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient import okhttp3.Request import org.koin.core.component.KoinComponent import org.koin.core.component.get -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.medianOrNull -import java.io.File -import java.io.InputStream -import java.util.zip.ZipFile +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug object MangaUtils : KoinComponent { @@ -53,9 +53,7 @@ object MangaUtils : KoinComponent { } return size.width * 2 < size.height } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() return null } } @@ -78,4 +76,4 @@ object MangaUtils : KoinComponent { check(imageHeight > 0 && imageWidth > 0) return Size(imageWidth, imageHeight) } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt index 64317e4a7..e43ca8877 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt @@ -7,10 +7,12 @@ import android.view.View import android.view.WindowManager import androidx.viewbinding.ViewBinding +@Suppress("DEPRECATION") private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +@Suppress("DEPRECATION") private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or @@ -18,7 +20,8 @@ private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY -abstract class BaseFullscreenActivity : BaseActivity(), +abstract class BaseFullscreenActivity : + BaseActivity(), View.OnSystemUiVisibilityChangeListener { override fun onCreate(savedInstanceState: Bundle?) { @@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity : BaseActivity(), showSystemUI() } + @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith") @Deprecated("Deprecated in Java") final override fun onSystemUiVisibilityChange(visibility: Int) { onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) } // TODO WindowInsetsControllerCompat works incorrect + @Suppress("DEPRECATION") protected fun hideSystemUI() { window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN } + @Suppress("DEPRECATION") protected fun showSystemUI() { window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt index 39233e28f..f4904f8ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt @@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug abstract class BaseViewModel : ViewModel() { @@ -34,9 +34,7 @@ abstract class BaseViewModel : ViewModel() { } private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> - if (BuildConfig.DEBUG) { - throwable.printStackTrace() - } + throwable.printStackTraceDebug() if (throwable !is CancellationException) { onError.postCall(throwable) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt index 7da9e309f..58d8d22c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt @@ -4,7 +4,5 @@ import org.koin.dsl.module val githubModule get() = module { - factory { - GithubRepository(get()) - } + factory { GithubRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt index 09557cb47..88304755b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt @@ -54,27 +54,23 @@ class VersionId( return result } - companion object { - - private fun variantWeight(variantType: String) = - when (variantType.lowercase(Locale.ROOT)) { - "a", "alpha" -> 1 - "b", "beta" -> 2 - "rc" -> 4 - "" -> 8 - else -> 0 - } - - fun parse(versionName: String): VersionId { - val parts = versionName.substringBeforeLast('-').split('.') - val variant = versionName.substringAfterLast('-', "") - return VersionId( - major = parts.getOrNull(0)?.toIntOrNull() ?: 0, - minor = parts.getOrNull(1)?.toIntOrNull() ?: 0, - build = parts.getOrNull(2)?.toIntOrNull() ?: 0, - variantType = variant.filter(Char::isLetter), - variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0 - ) - } + private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) { + "a", "alpha" -> 1 + "b", "beta" -> 2 + "rc" -> 4 + "" -> 8 + else -> 0 } +} + +fun VersionId(versionName: String): VersionId { + val parts = versionName.substringBeforeLast('-').split('.') + val variant = versionName.substringAfterLast('-', "") + return VersionId( + major = parts.getOrNull(0)?.toIntOrNull() ?: 0, + minor = parts.getOrNull(1)?.toIntOrNull() ?: 0, + build = parts.getOrNull(2)?.toIntOrNull() ?: 0, + variantType = variant.filter(Char::isLetter), + variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0, + ) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 185336542..dd5f57cd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.core.prefs -import android.annotation.TargetApi import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt new file mode 100644 index 000000000..88c62514c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.core.prefs + +import androidx.lifecycle.liveData +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.flow + +fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow { + var lastValue: T = valueProducer() + emit(lastValue) + observe().collect { + if (it == key) { + val value = valueProducer() + if (value != lastValue) { + emit(value) + } + lastValue = value + } + } +} + +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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt index bfc8b7b83..9ec51d479 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt @@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) { fun valueOf(id: Int) = values().firstOrNull { it.id == id } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt index 20a7bf0c3..fb3216cb2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.util.Log import kotlin.system.exitProcess +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler { @@ -13,7 +14,7 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught try { applicationContext.startActivity(intent) } catch (t: Throwable) { - t.printStackTrace() + t.printStackTraceDebug() } Log.e("CRASH", e.message, e) exitProcess(1) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index f6ecc6f0d..7762f512c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -191,7 +191,7 @@ class DetailsActivity : R.id.action_save -> { viewModel.manga.value?.let { val chaptersCount = it.chapters?.size ?: 0 - val branches = viewModel.branches.value.orEmpty() + val branches = viewModel.branches.value?.toList().orEmpty() if (chaptersCount > 5 || branches.size > 1) { showSaveConfirmation(it, chaptersCount, branches) } else { 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 index c1624a6b8..31007f673 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -1,131 +1,106 @@ package org.koitharu.kotatsu.details.ui -import androidx.core.os.LocaleListCompat import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData +import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import java.io.IOException +import java.util.* +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.BuildConfig 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.exceptions.MangaNotFoundException -import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.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.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import org.koitharu.kotatsu.utils.ext.iterator -import java.io.IOException +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class DetailsViewModel( - private val intent: MangaIntent, + intent: MangaIntent, private val historyRepository: HistoryRepository, - private val favouritesRepository: FavouritesRepository, + favouritesRepository: FavouritesRepository, private val localMangaRepository: LocalMangaRepository, private val trackingRepository: TrackingRepository, - private val mangaDataRepository: MangaDataRepository, + mangaDataRepository: MangaDataRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, ) : BaseViewModel() { + private val delegate = MangaDetailsDelegate( + intent = intent, + settings = settings, + mangaDataRepository = mangaDataRepository, + historyRepository = historyRepository, + localMangaRepository = localMangaRepository, + ) + private var loadingJob: Job - private val mangaData = MutableStateFlow(intent.manga) - private val selectedBranch = MutableStateFlow(null) val onShowToast = SingleLiveEvent() - private val history = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .flatMapLatest { mangaId -> - historyRepository.observeOne(mangaId) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) + private val history = historyRepository.observeOne(delegate.mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - private val favourite = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .flatMapLatest { mangaId -> - favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) + private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() } + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - private val newChapters = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .mapLatest { mangaId -> - trackingRepository.getNewChaptersCount(mangaId) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) + private val newChapters = viewModelScope.async(Dispatchers.Default) { + trackingRepository.getNewChaptersCount(delegate.mangaId) + } - // Remote manga for saved and saved for remote - private val relatedManga = MutableStateFlow(null) private val chaptersQuery = MutableStateFlow("") - private val chaptersReversed = settings.observe() - .filter { it == AppSettings.KEY_REVERSE_CHAPTERS } - .map { settings.chaptersReverse } - .onStart { emit(settings.chaptersReverse) } + private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - val manga = mangaData.filterNotNull() - .asLiveData(viewModelScope.coroutineContext) - val favouriteCategories = favourite - .asLiveData(viewModelScope.coroutineContext) - val newChaptersCount = newChapters - .asLiveData(viewModelScope.coroutineContext) - val readingHistory = history - .asLiveData(viewModelScope.coroutineContext) - val isChaptersReversed = chaptersReversed - .asLiveData(viewModelScope.coroutineContext) + val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext) + val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext) + val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) } + val readingHistory = history.asLiveData(viewModelScope.coroutineContext) + val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext) - val bookmarks = mangaData.flatMapLatest { + val bookmarks = delegate.manga.flatMapLatest { if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val onMangaRemoved = SingleLiveEvent() - val branches = mangaData.map { - it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() + val branches = delegate.manga.map { + val chapters = it?.chapters ?: return@map emptySet() + chapters.mapTo(TreeSet()) { x -> x.branch } }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val selectedBranchIndex = combine( branches.asFlow(), - selectedBranch + delegate.selectedBranch ) { branches, selected -> branches.indexOf(selected) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) - val isChaptersEmpty = mangaData.mapNotNull { m -> - m?.run { chapters.isNullOrEmpty() } + val isChaptersEmpty = delegate.manga.map { m -> + m?.chapters?.isEmpty() == true }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) val chapters = combine( combine( - mangaData.map { it?.chapters.orEmpty() }, - relatedManga, - history.map { it?.chapterId }, - newChapters, - selectedBranch - ) { chapters, related, currentId, newCount, branch -> - val relatedChapters = related?.chapters - if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) { - mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch) - } else { - mapChapters(chapters, relatedChapters, currentId, newCount, branch) - } + delegate.manga, + delegate.relatedManga, + history, + delegate.selectedBranch, + ) { manga, related, history, branch -> + delegate.mapChapters(manga, related, history, newChapters.await(), branch) }, chaptersReversed, chaptersQuery, @@ -134,7 +109,7 @@ class DetailsViewModel( }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) val selectedBranchValue: String? - get() = selectedBranch.value + get() = delegate.selectedBranch.value init { loadingJob = doLoad() @@ -146,7 +121,11 @@ class DetailsViewModel( } fun deleteLocal() { - val m = mangaData.value ?: return + 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) checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } @@ -171,11 +150,11 @@ class DetailsViewModel( } fun setSelectedBranch(branch: String?) { - selectedBranch.value = branch + delegate.selectedBranch.value = branch } fun getRemoteManga(): Manga? { - return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } + return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } } fun performChapterSearch(query: String?) { @@ -183,7 +162,7 @@ class DetailsViewModel( } fun onDownloadComplete(downloadedManga: Manga) { - val currentManga = mangaData.value ?: return + val currentManga = delegate.manga.value ?: return if (currentManga.id != downloadedManga.id) { return } @@ -194,142 +173,16 @@ class DetailsViewModel( runCatching { localMangaRepository.getDetails(downloadedManga) }.onSuccess { - relatedManga.value = it + delegate.relatedManga.value = it }.onFailure { - if (BuildConfig.DEBUG) { - it.printStackTrace() - } + it.printStackTraceDebug() } } } } private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - var manga = mangaDataRepository.resolveIntent(intent) - ?: throw MangaNotFoundException("Cannot find manga") - mangaData.value = manga - manga = MangaRepository(manga.source).getDetails(manga) - // find default branch - val hist = historyRepository.getOne(manga) - selectedBranch.value = if (hist != null) { - val currentChapter = manga.chapters?.find { it.id == hist.chapterId } - if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters) - } else { - predictBranch(manga.chapters) - } - mangaData.value = manga - relatedManga.value = runCatching { - if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null - MangaRepository(m.source).getDetails(m) - } else { - localMangaRepository.findSavedManga(manga) - } - }.onFailure { error -> - if (BuildConfig.DEBUG) error.printStackTrace() - }.getOrNull() - } - - private fun mapChapters( - chapters: List, - downloadedChapters: List?, - currentId: Long?, - newCount: Int, - branch: String?, - ): List { - val result = ArrayList(chapters.size) - val dateFormat = settings.getDateFormat() - val currentIndex = chapters.indexOfFirst { it.id == currentId } - val firstNewIndex = chapters.size - newCount - val downloadedIds = downloadedChapters?.mapToSet { 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, - dateFormat = dateFormat, - ) - } - 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 - val dateFormat = settings.getDateFormat() - 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, - dateFormat = dateFormat, - ) ?: chapter.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = true, - isDownloaded = false, - dateFormat = dateFormat, - ) - } - 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, - dateFormat = dateFormat, - ) - } else { - null - } - } - result.sortBy { it.chapter.number } - } - return result - } - - private fun predictBranch(chapters: List?): String? { - if (chapters.isNullOrEmpty()) { - return null - } - val groups = chapters.groupBy { it.branch } - 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 - } - } - return groups.maxByOrNull { it.value.size }?.key + delegate.doLoad() } private fun List.filterSearch(query: String): List { 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 new file mode 100644 index 000000000..07f03dbda --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -0,0 +1,184 @@ +package org.koitharu.kotatsu.details.ui + +import androidx.core.os.LocaleListCompat +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.exceptions.MangaNotFoundException +import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +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.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 +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug + +class MangaDetailsDelegate( + private val intent: MangaIntent, + private val settings: AppSettings, + private val mangaDataRepository: MangaDataRepository, + private val historyRepository: HistoryRepository, + private val localMangaRepository: LocalMangaRepository, +) { + + 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 MangaNotFoundException("Cannot find manga") + mangaData.value = manga + manga = MangaRepository(manga.source).getDetails(manga) + // find default branch + val hist = historyRepository.getOne(manga) + selectedBranch.value = if (hist != null) { + val currentChapter = manga.chapters?.find { it.id == hist.chapterId } + if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters) + } else { + predictBranch(manga.chapters) + } + mangaData.value = manga + relatedManga.value = runCatching { + if (manga.source == MangaSource.LOCAL) { + val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null + MangaRepository(m.source).getDetails(m) + } else { + localMangaRepository.findSavedManga(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 dateFormat = settings.getDateFormat() + val currentIndex = chapters.indexOfFirst { it.id == currentId } + val firstNewIndex = chapters.size - newCount + val downloadedIds = downloadedChapters?.mapToSet { 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, + dateFormat = dateFormat, + ) + } + 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 + val dateFormat = settings.getDateFormat() + 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, + dateFormat = dateFormat, + ) ?: chapter.toListItem( + isCurrent = i == currentIndex, + isUnread = i > currentIndex, + isNew = i >= firstNewIndex, + isMissing = true, + isDownloaded = false, + dateFormat = dateFormat, + ) + } + 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, + dateFormat = dateFormat, + ) + } else { + null + } + } + result.sortBy { it.chapter.number } + } + return result + } + + private fun predictBranch(chapters: List?): String? { + if (chapters.isNullOrEmpty()) { + return null + } + val groups = chapters.groupBy { it.branch } + 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 + } + } + return groups.maxByOrNull { it.value.size }?.key + } +} \ No newline at end of file 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 index 58335ed31..d079eb51f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore import okhttp3.OkHttpClient import okhttp3.Request import okio.IOException -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository @@ -24,6 +23,7 @@ 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.deleteAwait +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.waitForNetwork import org.koitharu.kotatsu.utils.progress.ProgressJob @@ -156,9 +156,7 @@ class DownloadManager( outState.value = DownloadState.Cancelled(startId, manga, cover) throw e } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() outState.value = DownloadState.Error(startId, manga, cover, e) } finally { withContext(NonCancellable) { 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 index e249e4dc5..a0c6c63dd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt @@ -3,10 +3,8 @@ package org.koitharu.kotatsu.download.ui import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.flatMapLatest @@ -17,7 +15,7 @@ import org.koin.android.ext.android.get import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding import org.koitharu.kotatsu.download.ui.service.DownloadService -import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection +import org.koitharu.kotatsu.utils.bindServiceWithLifecycle class DownloadsActivity : BaseActivity() { @@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity() { val adapter = DownloadsAdapter(lifecycleScope, get()) binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = adapter - LifecycleAwareServiceConnection.bindService( - this, - this, - Intent(this, DownloadService::class.java), - 0 + bindServiceWithLifecycle( + owner = this, + service = Intent(this, DownloadService::class.java), + flags = 0, ).service.flatMapLatest { binder -> (binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null) }.onEach { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 46dc79586..1e24d033f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -3,10 +3,11 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -70,9 +71,7 @@ class FavouritesCategoriesViewModel( return result } - private fun observeAllCategoriesVisible() = settings.observe() - .filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE } - .map { settings.isAllFavouritesVisible } - .onStart { emit(settings.isAllFavouritesVisible) } - .distinctUntilChanged() + private fun observeAllCategoriesVisible() = settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) { + isAllFavouritesVisible + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt index d840b783f..e64e36e5a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt @@ -16,7 +16,7 @@ fun categoryAD( clickListener.onItemClick(item.category, it) } @Suppress("ClickableViewAccessibility") - binding.imageViewHandle.setOnTouchListener { v, event -> + binding.imageViewHandle.setOnTouchListener { _, event -> if (event.actionMasked == MotionEvent.ACTION_DOWN) { clickListener.onItemLongClick(item.category, itemView) } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 42dd81e95..1e14aeeca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -2,14 +2,15 @@ package org.koitharu.kotatsu.history.ui import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.* -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.ShortcutsRepository 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 @@ -19,6 +20,8 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.onFirst +import java.util.* +import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, @@ -29,11 +32,7 @@ class HistoryListViewModel( val isGroupingEnabled = MutableLiveData() - private val historyGrouping = settings.observe() - .filter { it == AppSettings.KEY_HISTORY_GROUPING } - .map { settings.historyGrouping } - .onStart { emit(settings.historyGrouping) } - .distinctUntilChanged() + private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping } .onEach { isGroupingEnabled.postValue(it) } override val content = combine( 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 index 20f768c2f..6adc8c0d2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -4,16 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.base.ui.BaseViewModel 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.prefs.observeAsLiveData import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.MangaGridModel -import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel -import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct abstract class MangaListViewModel( private val settings: AppSettings, @@ -21,20 +19,15 @@ abstract class MangaListViewModel( abstract val content: LiveData> val listMode = MutableLiveData() - val gridScale = settings.observe() - .filter { it == AppSettings.KEY_GRID_SIZE } - .map { settings.gridSize / 100f } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) { - settings.gridSize / 100f - } + val gridScale = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_GRID_SIZE, + valueProducer = { gridSize / 100f }, + ) open fun onRemoveFilterTag(tag: MangaTag) = Unit - protected fun createListModeFlow() = settings.observe() - .filter { it == AppSettings.KEY_LIST_MODE } - .map { settings.listMode } - .onStart { emit(settings.listMode) } - .distinctUntilChanged() + protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode } .onEach { if (listMode.value != it) { listMode.postValue(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt index 86b72c738..c13fd3cfa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt @@ -13,7 +13,7 @@ fun currentFilterAD( val chipGroup = itemView as ChipsView - chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> + chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data -> listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index acba2466c..0cbb4fad7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -1,18 +1,18 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.annotation.WorkerThread +import java.util.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class FilterCoordinator( private val repository: RemoteMangaRepository, @@ -113,7 +113,7 @@ class FilterCoordinator( FilterItem.Sort(it, isSelected = it == state.sortOrder) } } - if(allTags.isLoading || allTags.isError || tags.isNotEmpty()) { + if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres, state.tags.size)) tags.mapTo(list) { FilterItem.Tag(it, isChecked = it in state.tags) @@ -153,9 +153,7 @@ class FilterCoordinator( runCatching { repository.getTags() }.onFailure { error -> - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() }.getOrNull() } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index 4e5115ac8..fc2dbad03 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -15,11 +15,11 @@ import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress class LocalListFragment : MangaListFragment(), ActivityResultCallback> { @@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index f2b98d7e0..17a2c25b2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsLiveData import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.SingleLiveEvent @@ -21,11 +22,11 @@ class MainViewModel( val onOpenReader = SingleLiveEvent() var defaultSection by settings::defaultSection - val isSuggestionsEnabled = settings.observe() - .filter { it == AppSettings.KEY_SUGGESTIONS } - .onStart { emit("") } - .map { settings.isSuggestionsEnabled } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val isSuggestionsEnabled = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_SUGGESTIONS, + valueProducer = { isSuggestionsEnabled } + ) val isResumeEnabled = historyRepository .observeHasItems() diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt new file mode 100644 index 000000000..289f44386 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.reader.data + +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter + +fun Manga.filterChapters(branch: String?): Manga { + if (chapters.isNullOrEmpty()) return this + return copy(chapters = chapters?.filter { it.branch == branch }) +} + +private fun Manga.copy(chapters: List?) = Manga( + id = id, + title = title, + altTitle = altTitle, + url = url, + publicUrl = publicUrl, + rating = rating, + isNsfw = isNsfw, + coverUrl = coverUrl, + tags = tags, + state = state, + author = author, + largeCoverUrl = largeCoverUrl, + description = description, + chapters = chapters, + source = source, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index c54ed94d6..e4a29b948 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -6,11 +6,13 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.* -import android.widget.Toast import androidx.activity.result.ActivityResultCallback import androidx.core.graphics.Insets -import androidx.core.view.* -import androidx.fragment.app.commit +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.transition.Slide import androidx.transition.TransitionManager @@ -37,11 +39,7 @@ 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.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState -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 import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.settings.SettingsActivity @@ -51,6 +49,8 @@ import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.hasGlobalPoint import org.koitharu.kotatsu.utils.ext.observeWithPrevious +import org.koitharu.kotatsu.utils.ext.postDelayed +import java.util.concurrent.TimeUnit class ReaderActivity : BaseFullscreenActivity(), @@ -75,13 +75,13 @@ class ReaderActivity : private lateinit var controlDelegate: ReaderControlDelegate private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private var gestureInsets: Insets = Insets.NONE - - private val reader - get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*> + private lateinit var readerManager: ReaderManager + private val hideUiRunnable = Runnable { setUiIsVisible(false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityReaderBinding.inflate(layoutInflater)) + readerManager = ReaderManager(supportFragmentManager, R.id.container) supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = GridTouchHelper(this, this) orientationHelper = ScreenOrientationHelper(this) @@ -91,6 +91,7 @@ class ReaderActivity : insetsDelegate.interceptingWindowInsetsListener = this orientationHelper.observeAutoOrientation() + .flowWithLifecycle(lifecycle) .onEach { binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it }.launchIn(lifecycleScope) @@ -113,33 +114,20 @@ class ReaderActivity : } private fun onInitReader(mode: ReaderMode) { - val currentReader = reader - when (mode) { - ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, WebtoonReaderFragment()) - } - } - ReaderMode.REVERSED -> if (currentReader !is ReversedReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, ReversedReaderFragment()) - } - } - ReaderMode.STANDARD -> if (currentReader !is PagerReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, PagerReaderFragment()) - } - } + if (readerManager.currentMode != mode) { + readerManager.replace(mode) } - binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).setIcon( - when (mode) { - ReaderMode.WEBTOON -> R.drawable.ic_script - ReaderMode.REVERSED -> R.drawable.ic_read_reversed - ReaderMode.STANDARD -> R.drawable.ic_book_page - } - ) - binding.appbarTop.postDelayed(1000) { - setUiIsVisible(false) + val iconRes = when (mode) { + ReaderMode.WEBTOON -> R.drawable.ic_script + ReaderMode.REVERSED -> R.drawable.ic_read_reversed + ReaderMode.STANDARD -> R.drawable.ic_book_page + } + binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run { + setIcon(iconRes) + setVisible(true) + } + if (binding.appbarTop.isVisible) { + lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1)) } } @@ -151,18 +139,8 @@ class ReaderActivity : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_reader_mode -> { - ReaderConfigDialog.show( - supportFragmentManager, - when (reader) { - is PagerReaderFragment -> ReaderMode.STANDARD - is WebtoonReaderFragment -> ReaderMode.WEBTOON - is ReversedReaderFragment -> ReaderMode.REVERSED - else -> { - showWaitWhileLoading() - return false - } - } - ) + val currentMode = readerManager.currentMode ?: return false + ReaderConfigDialog.show(supportFragmentManager, currentMode) } R.id.action_settings -> { startActivity(SettingsActivity.newReaderSettingsIntent(this)) @@ -184,17 +162,17 @@ class ReaderActivity : supportFragmentManager, pages, title?.toString().orEmpty(), - reader?.getCurrentState()?.page ?: -1 + readerManager.currentReader?.getCurrentState()?.page ?: -1, ) } else { - showWaitWhileLoading() + return false } } R.id.action_save_page -> { viewModel.getCurrentPage()?.also { page -> - viewModel.saveCurrentState(reader?.getCurrentState()) + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.saveCurrentPage(page, savePageRequest) - } ?: showWaitWhileLoading() + } ?: return false } R.id.action_bookmark -> { if (viewModel.isBookmarkAdded.value == true) { @@ -216,10 +194,14 @@ class ReaderActivity : val hasPages = !viewModel.content.value?.pages.isNullOrEmpty() binding.layoutLoading.isVisible = isLoading && !hasPages if (isLoading && hasPages) { - binding.toastView.show(R.string.loading_, true) + binding.toastView.show(R.string.loading_) } else { binding.toastView.hide() } + val menu = binding.toolbarBottom.menu + menu.findItem(R.id.action_bookmark).isVisible = hasPages + menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages + menu.findItem(R.id.action_save_page).isVisible = hasPages } private fun onError(e: Throwable) { @@ -279,14 +261,14 @@ class ReaderActivity : val index = pages.indexOfFirst { it.id == page.id } if (index != -1) { withContext(Dispatchers.Main) { - reader?.switchPageTo(index, true) + readerManager.currentReader?.switchPageTo(index, true) } } } } override fun onReaderModeChanged(mode: ReaderMode) { - viewModel.saveCurrentState(reader?.getCurrentState()) + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.switchMode(mode) } @@ -304,12 +286,6 @@ class ReaderActivity : } } - private fun showWaitWhileLoading() { - Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply { - setGravity(Gravity.CENTER, 0, 0) - }.show() - } - private fun setWindowSecure(isSecure: Boolean) { if (isSecure) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) @@ -358,7 +334,7 @@ class ReaderActivity : override fun onWindowInsetsChanged(insets: Insets) = Unit override fun switchPageBy(delta: Int) { - reader?.switchPageBy(delta) + readerManager.currentReader?.switchPageBy(delta) } override fun toggleUiVisibility() { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index f8c5d73c0..dbe853894 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -5,14 +5,16 @@ import android.view.SoundEffectConstants import android.view.View import androidx.lifecycle.LifecycleCoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.utils.GridTouchHelper -@Suppress("UNUSED_PARAMETER") class ReaderControlDelegate( - private val scope: LifecycleCoroutineScope, - private val settings: AppSettings, + scope: LifecycleCoroutineScope, + settings: AppSettings, private val listener: OnInteractionListener ) { @@ -20,12 +22,8 @@ class ReaderControlDelegate( private var isVolumeKeysSwitchEnabled: Boolean = false init { - settings.observe() - .filter { it == AppSettings.KEY_READER_SWITCHERS } - .map { settings.readerPageSwitch } - .onStart { emit(settings.readerPageSwitch) } - .distinctUntilChanged() - .flowOn(Dispatchers.IO) + settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch } + .flowOn(Dispatchers.Default) .onEach { isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it @@ -57,7 +55,7 @@ class ReaderControlDelegate( } } - fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) { + fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) { listener.switchPageBy(-1) true @@ -92,9 +90,11 @@ class ReaderControlDelegate( else -> false } - fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - return (isVolumeKeysSwitchEnabled && - (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)) + fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean { + return ( + isVolumeKeysSwitchEnabled && + (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) + ) } interface OnInteractionListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt new file mode 100644 index 000000000..c5497fe8a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.reader.ui + +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.reversed.ReversedReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment +import java.util.* + +class ReaderManager( + private val fragmentManager: FragmentManager, + @IdRes private val containerResId: Int, +) { + + private val modeMap = EnumMap>>(ReaderMode::class.java) + + init { + modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java + modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java + modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java + } + + val currentReader: BaseReader<*>? + get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*> + + val currentMode: ReaderMode? + get() { + val readerClass = currentReader?.javaClass ?: return null + return modeMap.entries.find { it.value == readerClass }?.key + } + + fun replace(newMode: ReaderMode) { + val readerClass = requireNotNull(modeMap[newMode]) + fragmentManager.commit { + replace(containerResId, readerClass, null, null) + } + } + + fun replace(reader: BaseReader<*>) { + fragmentManager.commit { replace(containerResId, reader) } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt index f9852f4c6..a2acc8df7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt @@ -1,13 +1,11 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context -import android.graphics.Color import android.util.AttributeSet import android.view.Gravity import android.view.ViewGroup import androidx.annotation.StringRes import androidx.core.view.isVisible -import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.transition.Fade import androidx.transition.Slide import androidx.transition.TransitionManager @@ -15,26 +13,28 @@ import androidx.transition.TransitionSet import com.google.android.material.textview.MaterialTextView class ReaderToastView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, ) : MaterialTextView(context, attrs, defStyleAttr) { private var hideRunnable = Runnable { hide() } - fun show(message: CharSequence, isLoading: Boolean) { + fun show(message: CharSequence) { removeCallbacks(hideRunnable) text = message setupTransition() isVisible = true } - fun show(@StringRes messageId: Int, isLoading: Boolean) { - show(context.getString(messageId), isLoading) + fun show(@StringRes messageId: Int) { + show(context.getString(messageId)) } fun showTemporary(message: CharSequence, duration: Long) { - show(message, false) + show(message) postDelayed(hideRunnable, duration) } @@ -49,7 +49,7 @@ class ReaderToastView @JvmOverloads constructor( super.onDetachedFromWindow() } - private fun setupTransition () { + private fun setupTransition() { val parentView = parent as? ViewGroup ?: return val transition = TransitionSet() .setOrdering(TransitionSet.ORDERING_TOGETHER) @@ -58,14 +58,4 @@ class ReaderToastView @JvmOverloads constructor( .addTransition(Fade()) TransitionManager.beginDelayedTransition(parentView, transition) } - - // FIXME use it as compound drawable - private fun createProgressDrawable(): CircularProgressDrawable { - val drawable = CircularProgressDrawable(context) - drawable.setStyle(CircularProgressDrawable.DEFAULT) - drawable.arrowEnabled = false - drawable.setColorSchemeColors(Color.WHITE) - drawable.centerRadius = lineHeight / 3f - return drawable - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index f9ba0655f..784c844ff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -8,9 +8,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import org.koin.core.component.KoinComponent -import org.koin.core.component.get -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent @@ -21,22 +18,25 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.os.ShortcutsRepository 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.core.prefs.ScreenshotsPolicy +import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.history.domain.HistoryRepository 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.PageLoader import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope import java.util.* +private const val BOUNDS_PAGE_OFFSET = 2 +private const val PAGES_TRIM_THRESHOLD = 120 +private const val PREFETCH_LIMIT = 10 + class ReaderViewModel( private val intent: MangaIntent, initialState: ReaderState?, @@ -78,22 +78,19 @@ class ReaderViewModel( val manga: Manga? get() = mangaData.value - val readerAnimation = settings.observe() - .filter { it == AppSettings.KEY_READER_ANIMATION } - .map { settings.readerAnimation } - .onStart { emit(settings.readerAnimation) } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) + val readerAnimation = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_READER_ANIMATION, + valueProducer = { readerAnimation } + ) val isScreenshotsBlockEnabled = combine( mangaData, - settings.observe() - .filter { it == AppSettings.KEY_SCREENSHOTS_POLICY } - .onStart { emit("") } - .map { settings.screenshotsPolicy }, + settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }, ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val onZoomChanged = SingleLiveEvent() @@ -142,7 +139,7 @@ class ReaderViewModel( if (state != null) { currentState.value = state } - saveState( + historyRepository.saveStateAsync( mangaData.value ?: return, state ?: currentState.value ?: return ) @@ -169,9 +166,7 @@ class ReaderViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() onPageSaved.postCall(null) } } @@ -286,7 +281,7 @@ class ReaderViewModel( } val branch = chapters[currentState.value?.chapterId ?: 0L].branch - mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch }) + mangaData.value = manga.filterChapters(branch) readerMode.postValue(mode) val pages = loadChapter(requireNotNull(currentState.value).chapterId) @@ -349,9 +344,9 @@ class ReaderViewModel( private fun subscribeToSettings() { settings.observe() - .filter { it == AppSettings.KEY_ZOOM_MODE } - .onEach { onZoomChanged.postCall(Unit) } - .launchIn(viewModelScope + Dispatchers.IO) + .onEach { key -> + if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit) + }.launchIn(viewModelScope + Dispatchers.Default) } private fun List.trySublist(fromIndex: Int, toIndex: Int): List { @@ -363,40 +358,23 @@ class ReaderViewModel( subList(fromIndexBounded, toIndexBounded) } } - - private fun Manga.copy(chapters: List?) = Manga( - id = id, - title = title, - altTitle = altTitle, - url = url, - publicUrl = publicUrl, - rating = rating, - isNsfw = isNsfw, - coverUrl = coverUrl, - tags = tags, - state = state, - author = author, - largeCoverUrl = largeCoverUrl, - description = description, - chapters = chapters, - source = source, - ) - - private companion object : KoinComponent { - - const val BOUNDS_PAGE_OFFSET = 2 - const val PAGES_TRIM_THRESHOLD = 120 - const val PREFETCH_LIMIT = 10 - - fun saveState(manga: Manga, state: ReaderState) { - processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) { - get().addOrUpdate( - manga = manga, - chapterId = state.chapterId, - page = state.page, - scroll = state.scroll - ) - } +} + +/** + * 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): Job { + return processLifecycleScope.launch(Dispatchers.Default) { + runCatching { + addOrUpdate( + manga = manga, + chapterId = state.chapterId, + page = state.page, + scroll = state.scroll + ) + }.onFailure { + it.printStackTraceDebug() } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index b2a540baa..9c9922598 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.widgets.ChipsView @@ -21,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug private const val FILTER_MIN_INTERVAL = 750L @@ -133,9 +133,7 @@ class RemoteListViewModel( } hasNextPage.value = list.isNotEmpty() } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() listError.value = e if (!mangaList.value.isNullOrEmpty()) { onError.postCall(e) @@ -158,4 +156,4 @@ class RemoteListViewModel( textSecondary = 0, actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt index 5b7bc661e..a9e2ab345 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt @@ -8,15 +8,6 @@ import android.net.Uri import androidx.activity.ComponentActivity import androidx.annotation.MainThread import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.security.cert.CertificateEncodingException -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.android.ext.android.get @@ -28,6 +19,16 @@ import org.koitharu.kotatsu.core.github.VersionId import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit class AppUpdateChecker(private val activity: ComponentActivity) { @@ -45,8 +46,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) { suspend fun checkNow() = runCatching { val version = repo.getLatestVersion() - val newVersionId = VersionId.parse(version.name) - val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) + val newVersionId = VersionId(version.name) + val currentVersionId = VersionId(BuildConfig.VERSION_NAME) val result = newVersionId > currentVersionId if (result) { withContext(Dispatchers.Main) { @@ -56,7 +57,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { settings.lastUpdateCheckTimestamp = System.currentTimeMillis() result }.onFailure { - it.printStackTrace() + it.printStackTraceDebug() }.getOrNull() @MainThread @@ -99,7 +100,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { PackageManager.GET_SIGNATURES ) } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() + e.printStackTraceDebug() return null } val signatures = packageInfo?.signatures @@ -109,7 +110,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { val cf = CertificateFactory.getInstance("X509") cf.generateCertificate(input) as X509Certificate } catch (e: CertificateException) { - e.printStackTrace() + e.printStackTraceDebug() return null } return try { @@ -117,10 +118,10 @@ class AppUpdateChecker(private val activity: ComponentActivity) { val publicKey: ByteArray = md.digest(c.encoded) publicKey.byte2HexFormatted() } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() + e.printStackTraceDebug() null } catch (e: CertificateEncodingException) { - e.printStackTrace() + e.printStackTraceDebug() null } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 384905df2..4ffa13c5b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -6,7 +6,6 @@ import androidx.preference.Preference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.parser.MangaRepository @@ -14,6 +13,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.serializableArgument import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.withArgs @@ -70,9 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { preference.title = getString(R.string.logged_in_as, username) }.onFailure { error -> preference.isEnabled = error is AuthRequiredException - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt index b41ebb205..53ad51cbc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt @@ -7,12 +7,13 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.preference.Preference import com.google.android.material.snackbar.Snackbar -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), +class BackupSettingsFragment : + BasePreferenceFragment(R.string.backup_restore), ActivityResultCallback { private val backupSelectCall = registerForActivityResult( @@ -34,9 +35,7 @@ class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), try { backupSelectCall.launch(arrayOf("*/*")) } catch (e: ActivityNotFoundException) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() Snackbar.make( listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT ).show() diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt index 03dd423ea..cedc875fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt @@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class LifecycleAwareServiceConnection private constructor( +class LifecycleAwareServiceConnection( private val host: Activity, ) : ServiceConnection, DefaultLifecycleObserver { @@ -31,19 +31,15 @@ class LifecycleAwareServiceConnection private constructor( super.onDestroy(owner) host.unbindService(this) } - - companion object { - - fun bindService( - host: Activity, - lifecycleOwner: LifecycleOwner, - service: Intent, - flags: Int, - ): LifecycleAwareServiceConnection { - val connection = LifecycleAwareServiceConnection(host) - host.bindService(service, connection, flags) - lifecycleOwner.lifecycle.addObserver(connection) - return connection - } - } +} + +fun Activity.bindServiceWithLifecycle( + owner: LifecycleOwner, + service: Intent, + flags: Int +): LifecycleAwareServiceConnection { + val connection = LifecycleAwareServiceConnection(this) + bindService(service, connection, flags) + owner.lifecycle.addObserver(connection) + return connection } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 6f15f7cd3..bd6ad731b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -9,7 +9,11 @@ import android.net.Uri import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope import androidx.work.CoroutineWorker +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -55,4 +59,11 @@ fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCo return runCatching { launch(input, options) }.isSuccess +} + +fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) { + coroutineScope.launch { + delay(delay) + runnable.run() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt index 412ead77c..dd4907134 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt @@ -3,15 +3,6 @@ package org.koitharu.kotatsu.utils.ext import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CoroutineExceptionHandler -import org.koitharu.kotatsu.BuildConfig - -val IgnoreErrors - get() = CoroutineExceptionHandler { _, e -> - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - } val processLifecycleScope: LifecycleCoroutineScope inline get() = ProcessLifecycleOwner.get().lifecycleScope \ 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 index ad1fe8b39..e3cd66b43 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.liveData +import kotlinx.coroutines.Deferred import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml index 978a29d74..569bffc11 100644 --- a/app/src/main/res/menu/opt_reader_bottom.xml +++ b/app/src/main/res/menu/opt_reader_bottom.xml @@ -9,12 +9,14 @@ android:id="@+id/action_bookmark" android:icon="@drawable/ic_bookmark" android:title="@string/bookmark_add" + android:visible="false" app:showAsAction="always" /> Date: Wed, 11 May 2022 12:11:37 +0300 Subject: [PATCH 03/64] Update parsers --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e899f7b87..e0f15b359 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 406 - versionName '3.2.2' + versionCode 407 + versionName '3.2.3' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -65,7 +65,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:b495e5e457') { + implementation('com.github.nv95:kotatsu-parsers:05a93e2380') { exclude group: 'org.json', module: 'json' } From e85b9db118c3a48869782fb73f888232a98cad51 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 9 May 2022 15:27:00 +0300 Subject: [PATCH 04/64] Show stub if favourite categories empty --- .../favourites/data/FavouriteCategoriesDao.kt | 2 +- .../favourites/domain/FavouritesRepository.kt | 11 ++++-- .../ui/FavouritesContainerFragment.kt | 38 +++++++++++++++++-- .../FavouritesCategoriesViewModel.kt | 2 +- .../ui/list/FavouritesListViewModel.kt | 12 ++++++ .../main/res/layout/fragment_favourites.xml | 6 +++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 63 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index 436dc12ea..9a0e4ed3f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -13,7 +13,7 @@ abstract class FavouriteCategoriesDao { abstract fun observeAll(): Flow> @Query("SELECT * FROM favourite_categories WHERE category_id = :id") - abstract fun observe(id: Long): Flow + abstract fun observe(id: Long): Flow @Insert(onConflict = OnConflictStrategy.ABORT) abstract suspend fun insert(category: FavouriteCategoryEntity): Long diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 731d7ff5e..026fec18b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -1,10 +1,7 @@ package org.koitharu.kotatsu.favourites.domain import androidx.room.withTransaction -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.model.FavouriteCategory @@ -48,6 +45,11 @@ class FavouritesRepository(private val db: MangaDatabase) { }.distinctUntilChanged() } + fun observeCategory(id: Long): Flow { + return db.favouriteCategoriesDao.observe(id) + .map { it?.toFavouriteCategory() } + } + fun observeCategories(mangaId: Long): Flow> { return db.favouritesDao.observe(mangaId).map { entity -> entity?.categories?.map { it.toFavouriteCategory() }.orEmpty() @@ -121,6 +123,7 @@ class FavouritesRepository(private val db: MangaDatabase) { private fun observeOrder(categoryId: Long): Flow { return db.favouriteCategoriesDao.observe(categoryId) + .filterNotNull() .map { x -> SortOrder(x.order, SortOrder.NEWEST) } .distinctUntilChanged() } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 4433f978d..81eb9c38c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -6,6 +6,7 @@ import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets import androidx.core.view.children +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -18,6 +19,7 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeListener import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding +import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel @@ -31,13 +33,15 @@ class FavouritesContainerFragment : BaseFragment(), FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback, - ActionModeListener { + ActionModeListener, + View.OnClickListener { private val viewModel by viewModel() private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { CategoriesEditDelegate(requireContext(), this) } private var pagerAdapter: FavouritesPagerAdapter? = null + private var stubBinding: ItemEmptyStateBinding? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -52,9 +56,7 @@ class FavouritesContainerFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val adapter = FavouritesPagerAdapter(this, this) - viewModel.visibleCategories.value?.let { - adapter.replaceData(it) - } + viewModel.visibleCategories.value?.let(::onCategoriesChanged) binding.pager.adapter = adapter pagerAdapter = adapter TabLayoutMediator(binding.tabs, binding.pager, adapter).attach() @@ -66,6 +68,7 @@ class FavouritesContainerFragment : override fun onDestroyView() { pagerAdapter = null + stubBinding = null super.onDestroyView() } @@ -101,6 +104,15 @@ class FavouritesContainerFragment : private fun onCategoriesChanged(categories: List) { pagerAdapter?.replaceData(categories) + if (categories.isEmpty()) { + binding.pager.isVisible = false + binding.tabs.isVisible = false + showStub() + } else { + binding.pager.isVisible = true + binding.tabs.isVisible = true + (stubBinding?.root ?: binding.stubEmptyState).isVisible = false + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -130,6 +142,12 @@ class FavouritesContainerFragment : return true } + override fun onClick(v: View) { + when (v.id) { + R.id.button_retry -> editDelegate.createCategory() + } + } + override fun onDeleteCategory(category: FavouriteCategory) { viewModel.deleteCategory(category.id) } @@ -193,6 +211,18 @@ class FavouritesContainerFragment : menu.show() } + private fun showStub() { + val stub = stubBinding ?: ItemEmptyStateBinding.bind(binding.stubEmptyState.inflate()) + stub.root.isVisible = true + stub.icon.setImageResource(R.drawable.ic_heart_outline) + stub.textPrimary.setText(R.string.text_empty_holder_primary) + stub.textSecondary.setText(R.string.empty_favourite_categories) + stub.buttonRetry.setText(R.string.add) + stub.buttonRetry.isVisible = true + stub.buttonRetry.setOnClickListener(this) + stubBinding = stub + } + companion object { fun newInstance() = FavouritesContainerFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 7aac74e62..625b63d33 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -31,7 +31,7 @@ class FavouritesCategoriesViewModel( repository.observeCategories(), observeAllCategoriesVisible(), ) { list, showAll -> - mapCategories(list, showAll, showAll) + mapCategories(list, showAll, showAll && list.isNotEmpty()) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) fun createCategory(name: String) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 476fa6fb1..157058560 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -1,9 +1,13 @@ package org.koitharu.kotatsu.favourites.ui.list +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView.NO_ID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository @@ -24,6 +28,14 @@ class FavouritesListViewModel( settings: AppSettings, ) : MangaListViewModel(settings), CountersProvider { + var sortOrder: LiveData = if (categoryId == NO_ID) { + MutableLiveData(null) + } else { + repository.observeCategory(categoryId) + .map { it?.order } + .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + } + override val content = combine( if (categoryId == 0L) { repository.observeAll(SortOrder.NEWEST) diff --git a/app/src/main/res/layout/fragment_favourites.xml b/app/src/main/res/layout/fragment_favourites.xml index 0f7e61994..d93d500b3 100644 --- a/app/src/main/res/layout/fragment_favourites.xml +++ b/app/src/main/res/layout/fragment_favourites.xml @@ -17,4 +17,10 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 41dad2021..5132657f0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -278,4 +278,5 @@ Главы будут удалены в фоновом режиме. Это может занять какое-то время Скрыть Доступны новые источники манги + Нет категорий избранного \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 692597cee..e1f0d34a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -281,4 +281,5 @@ Chapters will be removed in the background. It can take some time Hide New manga sources are available + No favourite categories \ No newline at end of file From 10c03ff01a57be518fb1204337ee512ddb6d2fde Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 11 May 2022 12:28:50 +0300 Subject: [PATCH 05/64] Update version in github templates --- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 97aa936c3..c640ff3c0 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -44,7 +44,7 @@ body: label: Kotatsu version description: You can find your Kotatsu version in **Settings → About**. placeholder: | - Example: "3.2.2" + Example: "3.2.3" validations: required: true @@ -87,7 +87,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index 2077efe27..bae1b501c 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file From 36634ecca19016cab9af572c8a662d4e74a34c8b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 11 May 2022 16:23:37 +0300 Subject: [PATCH 06/64] Option to undo removing from favourites --- app/build.gradle | 2 +- .../kotatsu/base/domain/ReversibleHandle.kt | 19 +++++++++++++++++++ .../kotatsu/history/data/HistoryDao.kt | 13 +++++++++++++ .../history/domain/HistoryRepository.kt | 14 ++++++++++++++ .../kotatsu/history/ui/HistoryListFragment.kt | 10 ++++++++++ .../history/ui/HistoryListViewModel.kt | 11 +++++++++-- app/src/main/res/values/strings.xml | 2 ++ 7 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt diff --git a/app/build.gradle b/app/build.gradle index 1c67c45f5..57228a164 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,7 +102,7 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'io.insert-koin:koin-android:3.2.0' - implementation 'io.coil-kt:coil-base:2.0.0-rc03' + implementation 'io.coil-kt:coil-base:2.0.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt new file mode 100644 index 000000000..43c9bf7e4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.base.domain + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.utils.ext.processLifecycleScope + +fun interface ReversibleHandle { + + suspend fun reverse() +} + +fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) { + reverse() +} + +operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle { + this.reverse() + other.reverse() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt index 062e8d898..f52c5db6d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -15,6 +15,10 @@ abstract class HistoryDao { @Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(offset: Int, limit: Int): List + @Transaction + @Query("SELECT * FROM history WHERE manga_id IN (:ids)") + abstract suspend fun findAll(ids: Collection): List + @Transaction @Query("SELECT * FROM history ORDER BY updated_at DESC") abstract fun observeAll(): Flow> @@ -69,4 +73,13 @@ abstract class HistoryDao { true } else false } + + @Transaction + open suspend fun upsert(entities: Iterable) { + for (e in entities) { + if (update(e) == 0) { + insert(e) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index a88c8a82d..a4b2ab772 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -4,6 +4,7 @@ import androidx.room.withTransaction import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged 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.* import org.koitharu.kotatsu.core.model.MangaHistory @@ -100,6 +101,19 @@ class HistoryRepository( } } + suspend fun deleteReversible(ids: Collection): ReversibleHandle { + val entities = db.withTransaction { + val entities = db.historyDao.findAll(ids.toList()).filterNotNull() + for (id in ids) { + db.historyDao.delete(id) + } + entities + } + return ReversibleHandle { + db.historyDao.upsert(entities) + } + } + /** * Try to replace one manga with another one * Useful for replacing saved manga on deleting it with remove source diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 6980b80ee..27f4a86ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -7,8 +7,11 @@ import android.view.MenuItem import android.view.View import androidx.appcompat.view.ActionMode import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.ReversibleHandle +import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource @@ -22,6 +25,7 @@ class HistoryListFragment : MangaListFragment() { viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } + viewModel.onItemsRemoved.observe(viewLifecycleOwner, ::onItemsRemoved) } override fun onScrolledToEnd() = Unit @@ -80,6 +84,12 @@ class HistoryListFragment : MangaListFragment() { } } + private fun onItemsRemoved(reversibleHandle: ReversibleHandle) { + Snackbar.make(binding.recyclerView, R.string.removed_from_history, Snackbar.LENGTH_LONG) + .setAction(R.string.undo) { reversibleHandle.reverseAsync() } + .show() + } + companion object { fun newInstance() = HistoryListFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 1e14aeeca..60190535b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -7,6 +7,8 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.domain.ReversibleHandle +import org.koitharu.kotatsu.base.domain.plus import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode @@ -17,6 +19,7 @@ import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.onFirst @@ -31,6 +34,7 @@ class HistoryListViewModel( ) : MangaListViewModel(settings) { val isGroupingEnabled = MutableLiveData() + val onItemsRemoved = SingleLiveEvent() private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping } .onEach { isGroupingEnabled.postValue(it) } @@ -72,9 +76,12 @@ class HistoryListViewModel( if (ids.isEmpty()) { return } - launchJob { - repository.delete(ids) + launchJob(Dispatchers.Default) { + val handle = repository.deleteReversible(ids) + ReversibleHandle { + shortcutsRepository.updateShortcuts() + } shortcutsRepository.updateShortcuts() + onItemsRemoved.postCall(handle) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13dde4b4e..bb74b8b83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -293,4 +293,6 @@ Bookmarks Bookmark removed Bookmark added + Undo + Removed from history \ No newline at end of file From 730d664b912f52d26d91113edfe5080d0a7e77ce Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 11 May 2022 17:05:49 +0300 Subject: [PATCH 07/64] Tune ui --- .../kotatsu/base/ui/widgets/ListItemTextView.kt | 7 +++---- .../select/FavouriteCategoriesBottomSheet.kt | 13 ++++++++++--- .../{MangaCaegoryAD.kt => MangaCategoryAD.kt} | 0 .../ui/suggestion/SearchSuggestionFragment.kt | 8 ++++---- .../res/layout/dialog_favorite_categories.xml | 17 ++++++++++++++--- app/src/main/res/layout/item_checkable_new.xml | 2 -- app/src/main/res/menu/opt_favourites_bs.xml | 13 ------------- app/src/main/res/values-v27/styles.xml | 1 + app/src/main/res/values/attrs.xml | 3 +-- app/src/main/res/values/styles.xml | 3 ++- 10 files changed, 35 insertions(+), 32 deletions(-) rename app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/{MangaCaegoryAD.kt => MangaCategoryAD.kt} (100%) delete mode 100644 app/src/main/res/menu/opt_favourites_bs.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt index 18d7262dc..bdaf8f476 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ListItemTextView.kt @@ -36,8 +36,7 @@ class ListItemTextView @JvmOverloads constructor( init { context.withStyledAttributes(attrs, R.styleable.ListItemTextView, defStyleAttr) { - val itemRippleColor = getColorStateList(R.styleable.ListItemTextView_rippleColor) - ?: getRippleColorFallback(context) + val itemRippleColor = getRippleColor(context) val shape = createShapeDrawable(this) background = RippleDrawable( RippleUtils.sanitizeRippleDrawableColor(itemRippleColor), @@ -108,7 +107,7 @@ class ListItemTextView @JvmOverloads constructor( ta.getResourceId(R.styleable.ListItemTextView_shapeAppearanceOverlay, 0), ).build() val shapeDrawable = MaterialShapeDrawable(shapeAppearance) - shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundTint) + shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundFillColor) return InsetDrawable( shapeDrawable, ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetLeft, 0), @@ -118,7 +117,7 @@ class ListItemTextView @JvmOverloads constructor( ) } - private fun getRippleColorFallback(context: Context): ColorStateList { + private fun getRippleColor(context: Context): ColorStateList { return context.getThemeColorStateList(android.R.attr.colorControlHighlight) ?: ColorStateList.valueOf(Color.TRANSPARENT) } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt index 9eb9b8d04..aa2bacbbe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesBottomSheet.kt @@ -28,7 +28,7 @@ class FavouriteCategoriesBottomSheet : BaseBottomSheet(), OnListItemClickListener, CategoriesEditDelegate.CategoriesEditCallback, - Toolbar.OnMenuItemClickListener { + Toolbar.OnMenuItemClickListener, View.OnClickListener { private val viewModel by viewModel { parametersOf(requireNotNull(arguments?.getParcelableArrayList(KEY_MANGA_LIST)).map { it.manga }) @@ -46,6 +46,7 @@ class FavouriteCategoriesBottomSheet : adapter = MangaCategoriesAdapter(this) binding.recyclerViewCategories.adapter = adapter binding.toolbar.setOnMenuItemClickListener(this) + binding.itemCreate.setOnClickListener(this) viewModel.content.observe(viewLifecycleOwner, this::onContentChanged) viewModel.onError.observe(viewLifecycleOwner, ::onError) @@ -58,14 +59,20 @@ class FavouriteCategoriesBottomSheet : override fun onMenuItemClick(item: MenuItem): Boolean { return when (item.itemId) { - R.id.action_create -> { - startActivity(FavouritesCategoryEditActivity.newIntent(requireContext())) + R.id.action_done -> { + dismiss() true } else -> false } } + override fun onClick(v: View) { + when (v.id) { + R.id.item_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext())) + } + } + override fun onItemClick(item: MangaCategoryItem, view: View) { viewModel.setChecked(item.id, !item.isChecked) } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCaegoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt similarity index 100% rename from app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCaegoryAD.kt rename to app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/MangaCategoryAD.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt index 9a1ffba8f..7d9a3b6cb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -9,6 +9,7 @@ import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding import org.koitharu.kotatsu.main.ui.AppBarOwner @@ -43,11 +44,10 @@ class SearchSuggestionFragment : override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top + val extraPadding = resources.getDimensionPixelOffset(R.dimen.list_spacing) binding.root.updatePadding( - top = headerHeight, - // left = insets.left, - // right = insets.right, - bottom = insets.bottom, + top = headerHeight + extraPadding, + bottom = insets.bottom + extraPadding, ) } diff --git a/app/src/main/res/layout/dialog_favorite_categories.xml b/app/src/main/res/layout/dialog_favorite_categories.xml index a808907bf..91cc561aa 100644 --- a/app/src/main/res/layout/dialog_favorite_categories.xml +++ b/app/src/main/res/layout/dialog_favorite_categories.xml @@ -5,13 +5,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical"> + android:orientation="vertical" + android:paddingBottom="@dimen/list_spacing"> + + diff --git a/app/src/main/res/layout/item_checkable_new.xml b/app/src/main/res/layout/item_checkable_new.xml index 9ec3eacf1..6e523dad8 100644 --- a/app/src/main/res/layout/item_checkable_new.xml +++ b/app/src/main/res/layout/item_checkable_new.xml @@ -1,7 +1,6 @@ \ No newline at end of file diff --git a/app/src/main/res/menu/opt_favourites_bs.xml b/app/src/main/res/menu/opt_favourites_bs.xml deleted file mode 100644 index 56e7d5723..000000000 --- a/app/src/main/res/menu/opt_favourites_bs.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml index 88dc7707b..370045cf3 100644 --- a/app/src/main/res/values-v27/styles.xml +++ b/app/src/main/res/values-v27/styles.xml @@ -3,5 +3,6 @@ \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c14ddff76..348cff5e0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,8 +26,7 @@ - - + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16f50044..7f96a0bef 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -24,6 +24,7 @@ @@ -86,7 +87,7 @@ + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f6addbe8f..69f2615b7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -86,9 +86,11 @@ + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 69f2615b7..18c2b0c84 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -35,6 +35,8 @@ @color/errorContainer @color/onErrorContainer + @color/divider_default + ?attr/colorSurfaceVariant @@ -81,8 +83,6 @@ - \ No newline at end of file From 790f1fb8a311d5e1fd6dc9a90ed9a4ea35d39014 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 12:06:26 +0300 Subject: [PATCH 32/64] Update parsers --- app/build.gradle | 4 ++-- .../org/koitharu/kotatsu/core/db/entity/EntityMapping.kt | 2 +- .../java/org/koitharu/kotatsu/local/data/PagesCache.kt | 6 +++--- .../kotatsu/local/domain/LocalMangaRepository.kt | 2 +- .../org/koitharu/kotatsu/reader/data/ModelMapping.kt | 4 ++-- .../java/org/koitharu/kotatsu/utils/ext/StringExt.kt | 9 +++++++++ 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e81717951..48da5314c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,7 +66,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:05a93e2380') { + implementation('com.github.nv95:kotatsu-parsers:f46c5add46') { exclude group: 'org.json', module: 'json' } @@ -103,7 +103,7 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'io.insert-koin:koin-android:3.2.0' - implementation 'io.coil-kt:coil-base:2.0.0' + implementation 'io.coil-kt:coil-base:2.1.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt index 51cd77b2e..5bdd0ca4a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/EntityMapping.kt @@ -3,9 +3,9 @@ package org.koitharu.kotatsu.core.db.entity import java.util.* import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase +import org.koitharu.kotatsu.utils.ext.longHashCode // Entity to model diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt index 73dd83bb4..7aee467a5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -2,13 +2,13 @@ package org.koitharu.kotatsu.local.data import android.content.Context import com.tomclaw.cache.DiskLruCache -import java.io.File -import java.io.InputStream import kotlinx.coroutines.flow.MutableStateFlow -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.subdir import org.koitharu.kotatsu.utils.ext.takeIfReadable +import java.io.File +import java.io.InputStream class PagesCache(context: Context) { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index e034d0672..2c0b37298 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -15,11 +15,11 @@ import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.TempFileFilter import org.koitharu.kotatsu.parsers.model.* -import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.toCamelCase 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.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.resolveName import java.io.File diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt index 289f44386..2cd9ffcb3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt @@ -5,10 +5,10 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter fun Manga.filterChapters(branch: String?): Manga { if (chapters.isNullOrEmpty()) return this - return copy(chapters = chapters?.filter { it.branch == branch }) + return withChapters(chapters = chapters?.filter { it.branch == branch }) } -private fun Manga.copy(chapters: List?) = Manga( +private fun Manga.withChapters(chapters: List?) = Manga( id = id, title = title, altTitle = altTitle, 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 index 48ba02122..badf5ae7c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -2,4 +2,13 @@ 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 From 58c9f75b91c80c8a44189732340cb9a1b2136a5b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 12:14:14 +0300 Subject: [PATCH 33/64] Fix tags order in filter --- .../org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index 1fbf4b70e..5ea361168 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -1,6 +1,7 @@ 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 @@ -28,7 +29,7 @@ class FilterCoordinator( } private var availableTagsDeferred = loadTagsAsync() - val items = getItemsFlow() + val items: LiveData> = getItemsFlow() .asLiveDataDistinct(coroutineScope.coroutineContext + Dispatchers.Default) init { @@ -105,7 +106,7 @@ class FilterCoordinator( query: String, ): List { val sortOrders = repository.sortOrders.sortedBy { it.ordinal } - val tags = mergeTags(state.tags, allTags.tags).sortedBy { it.title } + val tags = mergeTags(state.tags, allTags.tags).toList() val list = ArrayList(tags.size + sortOrders.size + 3) if (query.isEmpty()) { if (sortOrders.isNotEmpty()) { From 2b61b27271435b50a11b109bf1891a7931eb778c Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 17 May 2022 15:28:41 +0200 Subject: [PATCH 34/64] Translated using Weblate (Ukrainian) Currently translated at 100.0% (297 of 297 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Co-authored-by: Artem Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-uk/plurals.xml | 2 +- app/src/main/res/values-uk/strings.xml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/plurals.xml b/app/src/main/res/values-uk/plurals.xml index 0ced943b7..1b47f743e 100644 --- a/app/src/main/res/values-uk/plurals.xml +++ b/app/src/main/res/values-uk/plurals.xml @@ -2,7 +2,7 @@ %1$d новий розділ - %1$d нових розділи + %1$d нові розділи %1$d нових розділів %1$d нових розділів diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9fd8db95a..b26c58fd1 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -64,7 +64,7 @@ Кеш Б|кБ|МБ|ГБ|ТБ Стандартний - Манхва + Вебтун Режим читання Розмір сітки Пошук по %s @@ -290,4 +290,8 @@ Додано закладку Скасувати Видалено з історії + DNS через HTTPS + Типовий режим + Автоматично визначати, чи є манга вебтуном + Автовизначення режиму читання \ No newline at end of file From 312fb033e05e0e56261c3f4e4f074b3a074f922b Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 20 May 2022 18:40:53 +0300 Subject: [PATCH 35/64] Fix weird toolbar in category edit activity --- .../ui/categories/edit/FavouritesCategoryEditActivity.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt index 27239c0dc..b38dfec26 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt @@ -6,10 +6,12 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.core.graphics.Insets import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -84,9 +86,9 @@ class FavouritesCategoryEditActivity : BaseActivity right = insets.right, bottom = insets.bottom, ) - binding.toolbar.updatePadding( - top = insets.top, - ) + binding.toolbar.updateLayoutParams { + topMargin = insets.top + } } override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { From fc2820ec11209bb5e4dc3a46de7eb1afa1d46293 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 20 May 2022 19:12:58 +0300 Subject: [PATCH 36/64] Update version to v3.3 --- .gitignore | 1 + app/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3ba4daee9..418215154 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /.idea/dictionaries /.idea/modules.xml /.idea/misc.xml +/.idea/discord.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml diff --git a/app/build.gradle b/app/build.gradle index 48da5314c..e1c007038 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 408 - versionName '3.3-beta1' + versionCode 409 + versionName '3.3' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 0c07e649bfc32a50de8861169a21167f8e443ba8 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 21 May 2022 01:32:27 +0300 Subject: [PATCH 37/64] [Issue template] Update version --- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index c640ff3c0..e0a417b3f 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -44,7 +44,7 @@ body: label: Kotatsu version description: You can find your Kotatsu version in **Settings → About**. placeholder: | - Example: "3.2.3" + Example: "3.3" validations: required: true @@ -87,7 +87,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index bae1b501c..d4b373203 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.3](https://github.com/nv95/Kotatsu/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file From 4f3fef3bfed9b47515a5ed7265b6facbbfcfb2be Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 25 May 2022 10:04:06 +0300 Subject: [PATCH 38/64] Update parsers --- app/build.gradle | 9 ++++- .../kotatsu/core/parser/DummyParser.kt | 4 ++- .../kotatsu/core/model/MangaSource.kt | 10 ++++++ .../kotatsu/core/parser/MangaRepository.kt | 13 +++---- .../core/parser/RemoteMangaRepository.kt | 13 +++---- .../local/domain/LocalMangaRepository.kt | 35 ++++++++++-------- .../kotatsu/local/ui/LocalListViewModel.kt | 4 +-- .../search/domain/MangaSearchRepository.kt | 4 +-- .../newsources/NewSourcesViewModel.kt | 3 +- .../sources/SourcesSettingsViewModel.kt | 7 ++-- .../adapter/SourceConfigAdapterDelegates.kt | 15 ++++---- .../main/res/layout/item_source_config.xml | 36 ++++++++++++++----- app/src/main/res/layout/navigation_header.xml | 4 ++- 13 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt diff --git a/app/build.gradle b/app/build.gradle index e1c007038..108f28f92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,9 +64,16 @@ android { unitTests.returnDefaultValues = false } } +afterEvaluate { + compileDebugKotlin { + kotlinOptions { + freeCompilerArgs += ['-opt-in=kotlin.RequiresOptIn'] + } + } +} dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:f46c5add46') { + implementation('com.github.nv95:kotatsu-parsers:ab87a50e9b') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt index 4323e3a5f..479ada3ec 100644 --- a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt +++ b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.parser import java.util.* +import org.koitharu.kotatsu.parsers.InternalParsersApi import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey @@ -9,6 +10,7 @@ import org.koitharu.kotatsu.parsers.model.* /** * This parser is just for parser development, it should not be used in releases */ +@OptIn(InternalParsersApi::class) class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) { override val configKeyDomain: ConfigKey.Domain @@ -25,7 +27,7 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS offset: Int, query: String?, tags: Set?, - sortOrder: SortOrder? + sortOrder: SortOrder, ): List { TODO("Not yet implemented") } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt new file mode 100644 index 000000000..9bd4ef5cf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.core.model + +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.toTitleCase +import java.util.* + +fun MangaSource.getLocaleTitle(): String? { + val lc = Locale(locale ?: return null) + return lc.getDisplayLanguage(lc).toTitleCase(lc) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index ca4cc495b..812fc9e81 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -1,11 +1,11 @@ package org.koitharu.kotatsu.core.parser -import java.lang.ref.WeakReference -import java.util.* import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.* +import java.lang.ref.WeakReference +import java.util.* interface MangaRepository { @@ -13,12 +13,9 @@ interface MangaRepository { val sortOrders: Set - suspend fun getList( - offset: Int, - query: String? = null, - tags: Set? = null, - sortOrder: SortOrder? = null, - ): List + suspend fun getList(offset: Int, query: String?): List + + suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List suspend fun getDetails(manga: Manga): Manga diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 14a113e24..358bd2645 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -20,12 +20,13 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository { getConfig().defaultSortOrder = value } - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder?, - ): List = parser.getList(offset, query, tags, sortOrder) + override suspend fun getList(offset: Int, query: String?): List { + return parser.getList(offset, query) + } + + override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { + return parser.getList(offset, tags, sortOrder) + } override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 2c0b37298..6eef24c86 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -37,28 +37,25 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma private val filenameFilter = CbzFilter() private val locks = CompositeMutex() - override suspend fun getList( - offset: Int, - query: String?, - tags: Set?, - sortOrder: SortOrder? - ): List { + override suspend fun getList(offset: Int, query: String?): List { if (offset > 0) { return emptyList() } - val files = getAllFiles() - val list = coroutineScope { - val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) - files.map { file -> - getFromFileAsync(file, dispatcher) - }.awaitAll() - }.filterNotNullTo(ArrayList(files.size)) + val list = getRawList() if (!query.isNullOrEmpty()) { list.retainAll { x -> x.title.contains(query, ignoreCase = true) || x.altTitle?.contains(query, ignoreCase = true) == true } } + return list + } + + override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { + if (offset > 0) { + return emptyList() + } + val list = getRawList() if (!tags.isNullOrEmpty()) { list.retainAll { x -> x.tags.containsAll(tags) @@ -244,7 +241,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma } } - override val sortOrders = emptySet() + override val sortOrders = setOf(SortOrder.ALPHABETICAL) override suspend fun getPageUrl(page: MangaPage) = page.url @@ -295,6 +292,16 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma locks.unlock(id) } + private suspend fun getRawList(): ArrayList { + val files = getAllFiles() + return coroutineScope { + val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) + files.map { file -> + getFromFileAsync(file, dispatcher) + }.awaitAll() + }.filterNotNullTo(ArrayList(files.size)) + } + private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> dir.listFiles(filenameFilter)?.toList().orEmpty() } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 7ca7e8249..375e03997 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.local.ui import android.net.Uri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -24,6 +23,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress +import java.io.IOException class LocalListViewModel( private val repository: LocalMangaRepository, @@ -115,7 +115,7 @@ class LocalListViewModel( private suspend fun doRefresh() { try { listError.value = null - mangaList.value = repository.getList(0) + mangaList.value = repository.getList(0, null, null) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 270238553..2c95fb632 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings 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.levenshteinDistance import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider @@ -35,7 +34,6 @@ class MangaSearchRepository( MangaRepository(source).getList( offset = 0, query = query, - sortOrder = SortOrder.POPULARITY ) }.getOrElse { emptyList() @@ -141,4 +139,4 @@ class MangaSearchRepository( return false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt index 530851d46..08ef96c49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.newsources import androidx.lifecycle.MutableLiveData import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem @@ -33,7 +34,7 @@ class NewSourcesViewModel( sources.value = initialList.map { SourceConfigItem.SourceItem( source = it, - summary = null, + summary = it.getLocaleTitle(), isEnabled = it.name !in hidden, isDraggable = false, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt index 2f53c15fc..da3eba14f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt @@ -4,6 +4,7 @@ import androidx.core.os.LocaleListCompat import androidx.lifecycle.MutableLiveData import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toTitleCase @@ -82,7 +83,7 @@ class SourcesSettingsViewModel( } SourceConfigItem.SourceItem( source = it, - summary = null, + summary = it.getLocaleTitle(), isEnabled = it.name !in hiddenSources, isDraggable = false, ) @@ -105,7 +106,7 @@ class SourcesSettingsViewModel( enabledSources.mapTo(result) { SourceConfigItem.SourceItem( source = it, - summary = getLocaleTitle(it.locale), + summary = it.getLocaleTitle(), isEnabled = true, isDraggable = true, ) @@ -162,4 +163,4 @@ class SourcesSettingsViewModel( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index 752e3d33e..775da0f6e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -17,15 +17,17 @@ import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.utils.ext.enqueueWith +import org.koitharu.kotatsu.utils.ext.textAndVisible -fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } -) { +fun sourceConfigHeaderDelegate() = + adapterDelegateViewBinding( + { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } + ) { - bind { - binding.textViewTitle.setText(item.titleResId) + bind { + binding.textViewTitle.setText(item.titleResId) + } } -} fun sourceConfigGroupDelegate( listener: SourceConfigListener, @@ -61,6 +63,7 @@ fun sourceConfigItemDelegate( bind { binding.textViewTitle.text = item.source.title binding.switchToggle.isChecked = item.isEnabled + binding.textViewDescription.textAndVisible = item.summary imageRequest = ImageRequest.Builder(context) .data(item.faviconUrl) .error(R.drawable.ic_favicon_fallback) diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml index a0620433a..859105c96 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -3,8 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="?android:listPreferredItemHeightSmall" + android:layout_height="wrap_content" android:gravity="center_vertical" + android:minHeight="?android:listPreferredItemHeightSmall" android:orientation="horizontal"> - + android:orientation="vertical"> + + + + + + + android:src="@drawable/ic_totoro" + app:tint="?colorPrimary" /> Date: Mon, 30 May 2022 13:05:26 +0300 Subject: [PATCH 39/64] Update dependencies --- app/build.gradle | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 108f28f92..8f80d9a29 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,26 +77,24 @@ dependencies { exclude group: 'org.json', module: 'json' } - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2' - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.activity:activity-ktx:1.4.0' - implementation 'androidx.fragment:fragment-ktx:1.4.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-service:2.4.1' - implementation 'androidx.lifecycle:lifecycle-process:2.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.core:core-ktx:1.8.0-rc02' + implementation 'androidx.activity:activity-ktx:1.5.0-rc01' + implementation 'androidx.fragment:fragment-ktx:1.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-service:2.5.0-rc01' + implementation 'androidx.lifecycle:lifecycle-process:2.5.0-rc01' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'com.google.android.material:material:1.7.0-alpha01' + implementation 'com.google.android.material:material:1.7.0-alpha02' //noinspection LifecycleAnnotationProcessorWithJava8 - kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' + kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0-rc01' implementation 'androidx.room:room-runtime:2.4.2' implementation 'androidx.room:room-ktx:2.4.2' @@ -117,7 +115,7 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' testImplementation 'io.insert-koin:koin-test-junit4:3.2.0' androidTestImplementation 'androidx.test:runner:1.4.0' From a74b623c1084b4369d8063fbea121e0b8ce30746 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 30 May 2022 15:45:29 +0300 Subject: [PATCH 40/64] Refactor menu providers --- .../kotatsu/base/ui/BaseBottomSheet.kt | 2 +- .../base/ui/util/WindowInsetsDelegate.kt | 7 +-- .../kotatsu/details/ui/ChaptersFragment.kt | 59 +++++++++-------- .../kotatsu/details/ui/DetailsActivity.kt | 13 ---- .../kotatsu/details/ui/DetailsFragment.kt | 36 ++++++++--- .../ui/FavouritesContainerFragment.kt | 27 ++------ .../ui/FavouritesContainerMenuProvider.kt | 28 +++++++++ .../ui/list/FavouritesListFragment.kt | 42 +------------ .../ui/list/FavouritesListMenuProvider.kt | 48 ++++++++++++++ .../kotatsu/history/ui/HistoryListFragment.kt | 35 +---------- .../history/ui/HistoryListMenuProvider.kt | 41 ++++++++++++ .../kotatsu/list/ui/MangaListFragment.kt | 20 +----- .../kotatsu/list/ui/MangaListMenuProvider.kt | 25 ++++++++ .../kotatsu/local/ui/LocalListFragment.kt | 18 +----- .../kotatsu/local/ui/LocalListMenuProvider.kt | 26 ++++++++ .../kotatsu/reader/ui/PageSaveContract.kt | 2 +- .../remotelist/ui/RemoteListFragment.kt | 49 ++++++++------- .../settings/backup/BackupDialogFragment.kt | 17 ++--- .../sources/SourcesSettingsFragment.kt | 63 ++++++++++--------- .../suggestions/ui/SuggestionsFragment.kt | 35 ++++++----- .../kotatsu/tracker/ui/FeedFragment.kt | 42 ++----------- .../kotatsu/tracker/ui/FeedMenuProvider.kt | 48 ++++++++++++++ .../koitharu/kotatsu/utils/ext/FragmentExt.kt | 6 ++ .../koitharu/kotatsu/utils/ext/LocaleExt.kt | 8 ++- 24 files changed, 394 insertions(+), 303 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt index 7378b5acf..23ec29339 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt @@ -45,7 +45,7 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { requireContext().displayCompat?.let { val metrics = DisplayMetrics() it.getRealMetrics(metrics) - behavior?.peekHeight = metrics.heightPixels / 2 + behavior?.peekHeight = (metrics.heightPixels * 0.4).toInt() } return binding.root diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt index 5f1f0cd5e..c0d9c8c53 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/util/WindowInsetsDelegate.kt @@ -16,10 +16,7 @@ class WindowInsetsDelegate( private var lastInsets: Insets? = null - override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? { - if (insets == null) { - return null - } + override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets val newInsets = if (handleImeInsets) { Insets.max( @@ -49,7 +46,7 @@ class WindowInsetsDelegate( ) { view.removeOnLayoutChangeListener(this) if (lastInsets == null) { // Listener may not be called - onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view)) + onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view) ?: return) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index a9a57e30e..283a141d3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -27,6 +28,7 @@ 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.addMenuProvider import kotlin.math.roundToInt class ChaptersFragment : @@ -43,11 +45,6 @@ class ChaptersFragment : private var actionMode: ActionMode? = null private var selectionDecoration: ChaptersSelectionDecoration? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -72,6 +69,7 @@ class ChaptersFragment : binding.textViewHolder.isVisible = it activity?.invalidateOptionsMenu() } + addMenuProvider(ChaptersMenuProvider()) } override fun onDestroyView() { @@ -81,31 +79,6 @@ class ChaptersFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_chapters, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true - menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_reversed -> { - viewModel.setChaptersReversed(!item.isChecked) - true - } - else -> super.onOptionsItemSelected(item) - } - override fun onItemClick(item: ChapterListItem, view: View) { if (selectionDecoration?.checkedItemsCount != 0) { selectionDecoration?.toggleItemChecked(item.chapter.id) @@ -268,4 +241,30 @@ class ChaptersFragment : private fun onLoadingStateChanged(isLoading: Boolean) { binding.progressBar.isVisible = isLoading } + + private inner class ChaptersMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_chapters, menu) + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.setOnActionExpandListener(this@ChaptersFragment) + val searchView = searchMenuItem.actionView as SearchView + searchView.setOnQueryTextListener(this@ChaptersFragment) + searchView.setIconifiedByDefault(false) + searchView.queryHint = searchMenuItem.title + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true + menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_reversed -> { + viewModel.setChaptersReversed(!menuItem.isChecked) + true + } + else -> false + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index ec4b8e499..dc5e6a513 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -15,8 +15,6 @@ import android.widget.Toast import androidx.appcompat.view.ActionMode import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.Insets -import androidx.core.net.toFile -import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding @@ -45,7 +43,6 @@ import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity -import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage class DetailsActivity : @@ -166,16 +163,6 @@ class DetailsActivity : } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_share -> { - viewModel.manga.value?.let { - if (it.source == MangaSource.LOCAL) { - ShareHelper(this).shareCbz(listOf(it.url.toUri().toFile())) - } else { - ShareHelper(this).shareMangaLink(it) - } - } - true - } R.id.action_delete -> { val title = viewModel.manga.value?.title.orEmpty() MaterialAlertDialogBuilder(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 54ae95a5b..dfc427d99 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -8,8 +8,10 @@ import android.view.* import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.core.graphics.Insets +import androidx.core.net.toFile import androidx.core.net.toUri import androidx.core.text.parseAsHtml +import androidx.core.view.MenuProvider import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updatePadding @@ -40,6 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderState 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.ShareHelper import org.koitharu.kotatsu.utils.ext.* class DetailsFragment : @@ -52,11 +55,6 @@ class DetailsFragment : private val viewModel by sharedViewModel() private val coil by inject(mode = LazyThreadSafetyMode.NONE) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup?, @@ -76,11 +74,7 @@ class DetailsFragment : viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_details_info, menu) + addMenuProvider(DetailsMenuProvider()) } override fun onItemClick(item: Bookmark, view: View) { @@ -329,4 +323,26 @@ class DetailsFragment : } ?: request.fallback(R.drawable.ic_placeholder) request.enqueueWith(coil) } + + private inner class DetailsMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_details_info, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_share -> { + viewModel.manga.value?.let { + val context = requireContext() + if (it.source == MangaSource.LOCAL) { + ShareHelper(context).shareCbz(listOf(it.url.toUri().toFile())) + } else { + ShareHelper(context).shareMangaLink(it) + } + } + true + } + else -> false + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 8e9d945a0..22939d9d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.favourites.ui import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.Insets @@ -19,12 +21,12 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeListener import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding -import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.main.ui.AppBarOwner +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.resolveDp @@ -43,11 +45,6 @@ class FavouritesContainerFragment : private var pagerAdapter: FavouritesPagerAdapter? = null private var stubBinding: ItemEmptyStateBinding? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -61,6 +58,7 @@ class FavouritesContainerFragment : pagerAdapter = adapter TabLayoutMediator(binding.tabs, binding.pager, adapter).attach() actionModeDelegate.addListener(this, viewLifecycleOwner) + addMenuProvider(FavouritesContainerMenuProvider(view.context)) viewModel.visibleCategories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.onError.observe(viewLifecycleOwner, ::onError) @@ -115,21 +113,6 @@ class FavouritesContainerFragment : } } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_favourites, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_categories -> { - context?.let { - startActivity(CategoriesActivity.newIntent(it)) - } - true - } - else -> super.onOptionsItemSelected(item) - } - private fun onError(e: Throwable) { Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt new file mode 100644 index 000000000..1b07f535d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerMenuProvider.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.favourites.ui + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity + +class FavouritesContainerMenuProvider( + private val context: Context, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_favourites, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_categories -> { + context.startActivity(CategoriesActivity.newIntent(context)) + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index 8d4b9e419..4d55137a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -2,18 +2,15 @@ package org.koitharu.kotatsu.favourites.ui.list 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.iterator import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.titleRes -import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity 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 class FavouritesListFragment : MangaListFragment() { @@ -30,46 +27,13 @@ class FavouritesListFragment : MangaListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } - } - - override fun onScrolledToEnd() = Unit - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) if (categoryId != NO_ID) { - inflater.inflate(R.menu.opt_favourites_list, menu) - menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> - for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { - val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) - menuItem.isCheckable = true - } - submenu.setGroupCheckable(R.id.group_order, true, true) - } + addMenuProvider(FavouritesListMenuProvider(viewModel)) } } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> - val selectedOrder = viewModel.sortOrder.value - for (item in submenu) { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) - item.isChecked = order == selectedOrder - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when { - item.itemId == R.id.action_order -> false - item.groupId == R.id.group_order -> { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false - viewModel.setSortOrder(order) - true - } - else -> super.onOptionsItemSelected(item) - } - } + override fun onScrolledToEnd() = Unit override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_favourites, menu) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt new file mode 100644 index 000000000..2a1b08876 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListMenuProvider.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.favourites.ui.list + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import androidx.core.view.iterator +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity + +class FavouritesListMenuProvider( + private val viewModel: FavouritesListViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_favourites_list, menu) + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { + val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) + menuItem.isCheckable = true + } + submenu.setGroupCheckable(R.id.group_order, true, true) + } + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + val selectedOrder = viewModel.sortOrder.value + for (item in submenu) { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) + item.isChecked = order == selectedOrder + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when { + menuItem.itemId == R.id.action_order -> false + menuItem.groupId == R.id.group_order -> { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(menuItem.order) ?: return false + viewModel.setSortOrder(order) + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 27f4a86ca..b68f247aa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -2,11 +2,9 @@ package org.koitharu.kotatsu.history.ui 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 com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R @@ -14,6 +12,7 @@ import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.reverseAsync import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.addMenuProvider class HistoryListFragment : MangaListFragment() { @@ -22,6 +21,7 @@ class HistoryListFragment : MangaListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + addMenuProvider(HistoryListMenuProvider(view.context, viewModel)) viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } @@ -30,37 +30,6 @@ class HistoryListFragment : MangaListFragment() { override fun onScrolledToEnd() = Unit - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_history, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - menu.findItem(R.id.action_history_grouping)?.isChecked = - viewModel.isGroupingEnabled.value == true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_clear_history -> { - MaterialAlertDialogBuilder(context ?: return false) - .setTitle(R.string.clear_history) - .setMessage(R.string.text_clear_history_prompt) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.clear) { _, _ -> - viewModel.clearHistory() - }.show() - true - } - R.id.action_history_grouping -> { - viewModel.setGrouping(!item.isChecked) - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_history, menu) return super.onCreateActionMode(mode, menu) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt new file mode 100644 index 000000000..b27629ce6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.history.ui + +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 HistoryListMenuProvider( + private val context: Context, + private val viewModel: HistoryListViewModel, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_history, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_clear_history -> { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.clear_history) + .setMessage(R.string.text_clear_history_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewModel.clearHistory() + }.show() + true + } + R.id.action_history_grouping -> { + viewModel.setGrouping(!menuItem.isChecked) + true + } + else -> false + } + + override fun onPrepareMenu(menu: Menu) { + menu.findItem(R.id.action_history_grouping).isChecked = viewModel.isGroupingEnabled.value == true + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index ad7e3ab70..28556ac48 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -9,6 +9,7 @@ import androidx.collection.ArraySet import androidx.core.graphics.Insets import androidx.core.view.isNotEmpty import androidx.core.view.updatePadding +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar @@ -67,11 +68,6 @@ abstract class MangaListFragment : protected val selectedItems: Set get() = collectSelectedItems() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -98,6 +94,7 @@ abstract class MangaListFragment : setOnRefreshListener(this@MangaListFragment) isEnabled = isSwipeRefreshEnabled } + addMenuProvider(MangaListMenuProvider(childFragmentManager)) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) @@ -114,19 +111,6 @@ abstract class MangaListFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.opt_list, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.action_list_mode -> { - ListModeSelectDialog.show(childFragmentManager) - true - } - else -> super.onOptionsItemSelected(item) - } - override fun onItemClick(item: Manga, view: View) { if (selectionDecoration?.checkedItemsCount != 0) { selectionDecoration?.toggleItemChecked(item.id) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt new file mode 100644 index 000000000..5950cd546 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListMenuProvider.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.list.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import androidx.fragment.app.FragmentManager +import org.koitharu.kotatsu.R + +class MangaListMenuProvider( + private val fragmentManager: FragmentManager, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_list, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_list_mode -> { + ListModeSelectDialog.show(fragmentManager) + true + } + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index fc2dbad03..4fff18ac6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -4,7 +4,6 @@ import android.content.* import android.net.Uri import android.os.Bundle import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.activity.result.ActivityResultCallback @@ -19,6 +18,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress @@ -48,6 +48,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback { - onEmptyActionClick() - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onActivityResult(result: List<@JvmSuppressWildcards Uri>) { if (result.isEmpty()) return viewModel.importFiles(result) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt new file mode 100644 index 000000000..ce9941293 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListMenuProvider.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.local.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import org.koitharu.kotatsu.R + +class LocalListMenuProvider( + private val onImportClick: Function0, +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_local, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_import -> { + onImportClick() + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt index 556a03cac..406fbbd63 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveContract.kt @@ -9,7 +9,7 @@ import android.webkit.MimeTypeMap import androidx.activity.result.contract.ActivityResultContracts import androidx.core.net.toUri -class PageSaveContract : ActivityResultContracts.CreateDocument() { +class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") { override fun createIntent(context: Context, input: String): Intent { val intent = super.createIntent(context, input) diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 16a3b16a6..6dcd26b0f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,9 +1,12 @@ package org.koitharu.kotatsu.remotelist.ui +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 org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R @@ -11,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.serializableArgument import org.koitharu.kotatsu.utils.ext.withArgs @@ -22,29 +26,13 @@ class RemoteListFragment : MangaListFragment() { private val source by serializableArgument(ARG_SOURCE) - override fun onScrolledToEnd() { - viewModel.loadNextPage() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_list_remote, menu) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + addMenuProvider(RemoteListMenuProvider()) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_source_settings -> { - startActivity( - SettingsActivity.newSourceSettingsIntent(context ?: return false, source) - ) - true - } - R.id.action_filter -> { - onFilterClick() - true - } - else -> super.onOptionsItemSelected(item) - } + override fun onScrolledToEnd() { + viewModel.loadNextPage() } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { @@ -60,6 +48,25 @@ class RemoteListFragment : MangaListFragment() { viewModel.resetFilter() } + private inner class RemoteListMenuProvider: MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_list_remote, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_source_settings -> { + startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), source)) + true + } + R.id.action_filter -> { + onFilterClick() + true + } + else -> false + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 88f30c87b..b309a588b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -23,15 +23,16 @@ class BackupDialogFragment : AlertDialogFragment() { private val viewModel by viewModel() private var backup: File? = null - private val saveFileContract = - registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri -> - val file = backup - if (uri != null && file != null) { - saveBackup(file, uri) - } else { - dismiss() - } + private val saveFileContract = registerForActivityResult( + ActivityResultContracts.CreateDocument("*/*") + ) { uri -> + val file = backup + if (uri != null && file != null) { + saveBackup(file, uri) + } else { + dismiss() } + } override fun onInflateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index e89e20173..e8044fdde 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.* import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets +import androidx.core.view.MenuProvider import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView @@ -20,12 +21,11 @@ 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 class SourcesSettingsFragment : BaseFragment(), SourceConfigListener, - SearchView.OnQueryTextListener, - MenuItem.OnActionExpandListener, RecyclerViewOwner { private var reorderHelper: ItemTouchHelper? = null @@ -34,11 +34,6 @@ class SourcesSettingsFragment : override val recyclerView: RecyclerView get() = binding.recyclerView - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -62,6 +57,7 @@ class SourcesSettingsFragment : viewModel.items.observe(viewLifecycleOwner) { sourcesAdapter.items = it } + addMenuProvider(SourcesMenuProvider()) } override fun onDestroyView() { @@ -69,17 +65,6 @@ class SourcesSettingsFragment : super.onDestroyView() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_sources, menu) - val searchMenuItem = menu.findItem(R.id.action_search) - searchMenuItem.setOnActionExpandListener(this) - val searchView = searchMenuItem.actionView as SearchView - searchView.setOnQueryTextListener(this) - searchView.setIconifiedByDefault(false) - searchView.queryHint = searchMenuItem.title - } - override fun onWindowInsetsChanged(insets: Insets) { binding.recyclerView.updatePadding( bottom = insets.bottom, @@ -106,21 +91,39 @@ class SourcesSettingsFragment : viewModel.expandOrCollapse(header.localeId) } - override fun onQueryTextSubmit(query: String?): Boolean = false + private inner class SourcesMenuProvider : + MenuProvider, + MenuItem.OnActionExpandListener, + SearchView.OnQueryTextListener { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_sources, menu) + val searchMenuItem = menu.findItem(R.id.action_search) + searchMenuItem.setOnActionExpandListener(this) + val searchView = searchMenuItem.actionView as SearchView + searchView.setOnQueryTextListener(this) + searchView.setIconifiedByDefault(false) + searchView.queryHint = searchMenuItem.title + } - override fun onQueryTextChange(newText: String?): Boolean { - viewModel.performSearch(newText) - return true - } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { - (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) - return true - } + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + (activity as? AppBarOwner)?.appBar?.setExpanded(false, true) + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + (item.actionView as SearchView).setQuery("", false) + return true + } + + override fun onQueryTextSubmit(query: String?): Boolean = false - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - (item.actionView as SearchView).setQuery("", false) - return true + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.performSearch(newText) + return true + } } private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback( diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index ceb873f49..f1af41bf3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -4,30 +4,40 @@ 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 com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R 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 viewModel() override val isSwipeRefreshEnabled = false - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + addMenuProvider(SuggestionMenuProvider()) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_suggestions, menu) + override fun onScrolledToEnd() = Unit + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.mode_remote, menu) + return super.onCreateActionMode(mode, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { + private inner class SuggestionMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_suggestions, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.action_update -> { SuggestionsWorker.startNow(requireContext()) Snackbar.make( @@ -41,17 +51,10 @@ class SuggestionsFragment : MangaListFragment() { startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext())) true } - else -> super.onOptionsItemSelected(item) + else -> false } } - override fun onScrolledToEnd() = Unit - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.mode_remote, menu) - return super.onCreateActionMode(mode, menu) - } - companion object { fun newInstance() = SuggestionsFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index 584e2a4ff..32120ea94 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -1,10 +1,11 @@ package org.koitharu.kotatsu.tracker.ui import android.os.Bundle -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -21,6 +22,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker +import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.progress.Progress @@ -37,11 +39,6 @@ class FeedFragment : private var paddingVertical = 0 private var paddingHorizontal = 0 - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - override fun onInflateView( inflater: LayoutInflater, container: ViewGroup? @@ -63,6 +60,7 @@ class FeedFragment : ) addItemDecoration(decoration) } + addMenuProvider(FeedMenuProvider(binding.recyclerView, viewModel)) viewModel.content.observe(viewLifecycleOwner, this::onListChanged) viewModel.onError.observe(viewLifecycleOwner, this::onError) @@ -73,36 +71,6 @@ class FeedFragment : .observe(viewLifecycleOwner, this::onUpdateProgressChanged) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_feed, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_update -> { - TrackWorker.startNow(requireContext()) - Snackbar.make( - binding.recyclerView, - R.string.feed_will_update_soon, - Snackbar.LENGTH_LONG, - ).show() - true - } - R.id.action_clear_feed -> { - MaterialAlertDialogBuilder(context ?: return false) - .setTitle(R.string.clear_updates_feed) - .setMessage(R.string.text_clear_updates_feed_prompt) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.clear) { _, _ -> - viewModel.clearFeed() - }.show() - true - } - else -> super.onOptionsItemSelected(item) - } - } - override fun onDestroyView() { feedAdapter = null updateStatusSnackbar = null diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt new file mode 100644 index 000000000..6787ff7da --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.tracker.ui + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.tracker.work.TrackWorker + +class FeedMenuProvider( + private val snackbarHost: View, + private val viewModel: FeedViewModel, +) : MenuProvider { + + private val context: Context + get() = snackbarHost.context + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_feed, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_update -> { + TrackWorker.startNow(context) + Snackbar.make( + snackbarHost, + R.string.feed_will_update_soon, + Snackbar.LENGTH_LONG, + ).show() + true + } + R.id.action_clear_feed -> { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.clear_updates_feed) + .setMessage(R.string.text_clear_updates_feed_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewModel.clearFeed() + }.show() + true + } + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt index d881d3b1d..d37b579e7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt @@ -2,9 +2,11 @@ package org.koitharu.kotatsu.utils.ext import android.os.Bundle import android.os.Parcelable +import androidx.core.view.MenuProvider import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import java.io.Serializable @@ -43,4 +45,8 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) { if (!manager.isStateSaved) { show(manager, tag) } +} + +fun Fragment.addMenuProvider(provider: MenuProvider) { + requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt index b07b0c41d..ac878b538 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LocaleExt.kt @@ -3,12 +3,14 @@ package org.koitharu.kotatsu.utils.ext import androidx.core.os.LocaleListCompat import java.util.* -fun LocaleListCompat.toList(): List = createList(size()) { i -> get(i) } +fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw kotlin.NoSuchElementException() + +fun LocaleListCompat.toList(): List = createList(size()) { i -> getOrThrow(i) } operator fun LocaleListCompat.iterator() = object : Iterator { private var index = 0 override fun hasNext(): Boolean = index < size() - override fun next(): Locale = get(index++) + override fun next(): Locale = getOrThrow(index++) } inline fun > LocaleListCompat.mapTo( @@ -17,7 +19,7 @@ inline fun > LocaleListCompat.mapTo( ): C { val len = size() for (i in 0 until len) { - val item = get(i) + val item = get(i) ?: continue destination.add(block(item)) } return destination From ccb31de1ba338d663f16926f5a72c2c523c5daa9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 31 May 2022 16:22:30 +0300 Subject: [PATCH 41/64] Fix wrong tracker notifications --- .../kotatsu/core/model/MangaTracking.kt | 2 +- .../tracker/domain/TrackingRepository.kt | 25 +++++++++-------- .../kotatsu/tracker/work/TrackWorker.kt | 28 ++++++++----------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt index 77fdc5925..09af6176c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaTracking.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable +import java.util.* import kotlinx.parcelize.Parcelize import org.koitharu.kotatsu.parsers.model.Manga -import java.util.* data class MangaTracking( val manga: Manga, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index aefa9a69a..2048c38e0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -77,10 +77,11 @@ class TrackingRepository( suspend fun storeTrackResult( mangaId: Long, - knownChaptersCount: Int, - lastChapterId: Long, + knownChaptersCount: Int, // how many chapters user already seen + lastChapterId: Long, // in upstream manga newChapters: List, - previousTrackChapterId: Long + previousTrackChapterId: Long, // from previous check + saveTrackLog: Boolean, ) { db.withTransaction { val entity = TrackEntity( @@ -92,14 +93,16 @@ class TrackingRepository( lastNotifiedChapterId = newChapters.lastOrNull()?.id ?: previousTrackChapterId ) db.tracksDao.upsert(entity) - val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId } - if (foundChapters.isNotEmpty()) { - val logEntity = TrackLogEntity( - mangaId = mangaId, - chapters = foundChapters.joinToString("\n") { x -> x.name }, - createdAt = System.currentTimeMillis() - ) - db.trackLogsDao.insert(logEntity) + if (saveTrackLog && previousTrackChapterId != 0L) { + val foundChapters = newChapters.takeLastWhile { x -> x.id != previousTrackChapterId } + if (foundChapters.isNotEmpty()) { + val logEntity = TrackLogEntity( + mangaId = mangaId, + chapters = foundChapters.joinToString("\n") { x -> x.name }, + createdAt = System.currentTimeMillis() + ) + db.trackLogsDao.insert(logEntity) + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index f1e40a04b..ea249c7c4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.map import androidx.work.* import coil.ImageLoader import coil.request.ImageRequest -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent @@ -31,6 +30,7 @@ import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.progress.Progress +import java.util.concurrent.TimeUnit class TrackWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams), KoinComponent { @@ -65,25 +65,18 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : setProgress(workData.build()) val chapters = details?.chapters ?: continue when { - track.knownChaptersCount == -1 -> { // first check + // first check or manga was empty on last check + track.knownChaptersCount <= 0 || track.lastChapterId == 0L -> { repository.storeTrackResult( mangaId = track.manga.id, knownChaptersCount = chapters.size, lastChapterId = chapters.lastOrNull()?.id ?: 0L, previousTrackChapterId = 0L, - newChapters = emptyList() - ) - } - track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check - repository.storeTrackResult( - mangaId = track.manga.id, - knownChaptersCount = 0, - lastChapterId = 0L, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = chapters + newChapters = emptyList(), + saveTrackLog = false, ) - showNotification(details, channelId, chapters) } + // the same chapters count chapters.size == track.knownChaptersCount -> { if (chapters.lastOrNull()?.id == track.lastChapterId) { // manga was not updated. skip @@ -97,8 +90,9 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : mangaId = track.manga.id, knownChaptersCount = chapters.size, lastChapterId = chapters.lastOrNull()?.id ?: 0L, - previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = emptyList() + previousTrackChapterId = 0L, + newChapters = emptyList(), + saveTrackLog = false, ) } else { val newChapters = chapters.takeLast(chapters.size - knownChapter + 1) @@ -107,7 +101,8 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : knownChaptersCount = knownChapter + 1, lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters + newChapters = newChapters, + saveTrackLog = true, ) showNotification( details, @@ -125,6 +120,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, newChapters = newChapters, + saveTrackLog = true, ) showNotification( manga = track.manga, From 30c0fd600fcb61b978721c8f46347682f0d4449e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 31 May 2022 16:43:23 +0300 Subject: [PATCH 42/64] Fix global search results order --- .../core/exceptions/CompositeException.kt | 7 ++++ .../search/ui/multi/MultiSearchViewModel.kt | 37 ++++++++++++++----- 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt new file mode 100644 index 000000000..353ff1ca6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CompositeException.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.core.exceptions + +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet + +class CompositeException(val errors: Collection) : Exception( + message = errors.mapNotNullToSet { it.message }.joinToString() +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index 40b6a8619..adea07898 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update 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 @@ -78,8 +79,7 @@ class MultiSearchViewModel( listData.value = emptyList() loadingData.value = true query.postValue(q) - val errors = searchImpl(q) - listError.value = errors.firstOrNull() + searchImpl(q) } catch (e: Throwable) { listError.value = e } finally { @@ -88,25 +88,44 @@ class MultiSearchViewModel( } } - private suspend fun searchImpl(q: String): List { + private suspend fun searchImpl(q: String) { val sources = settings.getMangaSources(includeHidden = false) val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) - return coroutineScope { + val deferredList = coroutineScope { sources.map { source -> async(dispatcher) { runCatching { val list = MangaRepository(source).getList(offset = 0, query = q) - // .sortedBy { x -> x.title.levenshteinDistance(q) } .toUi(ListMode.GRID) if (list.isNotEmpty()) { - val item = MultiSearchListModel(source, list) - listData.update { x -> x + item } + MultiSearchListModel(source, list) + } else { + null } }.onFailure { it.printStackTraceDebug() - }.exceptionOrNull() + } } } - }.awaitAll().filterNotNull() + } + val errors = ArrayList() + for (deferred in deferredList) { + deferred.await() + .onSuccess { item -> + if (item != null) { + listData.update { x -> x + item } + } + }.onFailure { + errors.add(it) + } + } + if (listData.value.isNotEmpty()) { + return + } + when (errors.size) { + 0 -> Unit + 1 -> throw errors[0] + else -> throw CompositeException(errors) + } } } \ No newline at end of file From 3edfd0892a044959ee006a3f8e94570b70db9082 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 15 Jun 2022 13:35:24 +0300 Subject: [PATCH 43/64] Move tracker logic into own class --- .idea/gradle.xml | 2 +- app/build.gradle | 6 +- .../kotatsu/core/parser/DummyParser.kt | 4 +- .../kotatsu/core/db/dao/TrackLogsDao.kt | 2 +- .../koitharu/kotatsu/core/db/dao/TracksDao.kt | 3 +- .../core/exceptions/CompositeException.kt | 7 +- .../kotatsu/core/model/MangaTracking.kt | 37 +++- .../kotatsu/core/parser/MangaRepository.kt | 6 +- .../core/parser/RemoteMangaRepository.kt | 2 +- .../local/domain/LocalMangaRepository.kt | 16 +- .../koitharu/kotatsu/tracker/TrackerModule.kt | 3 + .../kotatsu/tracker/domain/Tracker.kt | 132 +++++++++++++ .../tracker/domain/TrackingRepository.kt | 11 +- .../tracker/domain/model/MangaUpdates.kt | 9 + .../kotatsu/tracker/work/TrackWorker.kt | 175 +++++------------- build.gradle | 2 +- 16 files changed, 254 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/domain/Tracker.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 6e5389ed9..a0de2a152 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -7,7 +7,7 @@