From b82b46f7d717895708032406b257904e517cb482 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 27 Jun 2022 16:06:25 +0300 Subject: [PATCH 1/6] Save reading percent to database --- .../kotatsu/bookmarks/data/BookmarkEntity.kt | 1 + .../kotatsu/bookmarks/data/EntityMapping.kt | 2 ++ .../kotatsu/bookmarks/domain/Bookmark.kt | 3 ++ .../kotatsu/core/backup/BackupRepository.kt | 1 + .../kotatsu/core/backup/RestoreRepository.kt | 8 ++--- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 13 ++++++-- .../core/db/migrations/Migration11To12.kt | 12 +++++++ .../kotatsu/core/model/MangaHistory.kt | 1 + .../kotatsu/history/data/EntityMapping.kt | 5 +-- .../kotatsu/history/data/HistoryEntity.kt | 5 +-- .../history/domain/HistoryRepository.kt | 3 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 33 ++++++++++++++----- 12 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt 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 index 0959b3362..1f348899b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt @@ -25,4 +25,5 @@ class BookmarkEntity( @ColumnInfo(name = "scroll") val scroll: Int, @ColumnInfo(name = "image") val imageUrl: String, @ColumnInfo(name = "created_at") val createdAt: Long, + @ColumnInfo(name = "percent") val percent: Float, ) \ 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 index 981aa05ea..0ab69dd18 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/EntityMapping.kt @@ -18,6 +18,7 @@ fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark( scroll = scroll, imageUrl = imageUrl, createdAt = Date(createdAt), + percent = percent, ) fun Bookmark.toEntity() = BookmarkEntity( @@ -28,4 +29,5 @@ fun Bookmark.toEntity() = BookmarkEntity( scroll = scroll, imageUrl = imageUrl, createdAt = createdAt.time, + percent = percent, ) \ 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 index 0b76c6537..5b6ff3bf0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt @@ -11,6 +11,7 @@ class Bookmark( val scroll: Int, val imageUrl: String, val createdAt: Date, + val percent: Float, ) { override fun equals(other: Any?): Boolean { @@ -26,6 +27,7 @@ class Bookmark( if (scroll != other.scroll) return false if (imageUrl != other.imageUrl) return false if (createdAt != other.createdAt) return false + if (percent != other.percent) return false return true } @@ -38,6 +40,7 @@ class Bookmark( result = 31 * result + scroll result = 31 * result + imageUrl.hashCode() result = 31 * result + createdAt.hashCode() + result = 31 * result + percent.hashCode() return result } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 4b42b4c60..7212b8569 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -111,6 +111,7 @@ class BackupRepository(private val db: MangaDatabase) { jo.put("chapter_id", chapterId) jo.put("page", page) jo.put("scroll", scroll) + jo.put("percent", percent) return jo } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt index 57fd4d6ee..4e20b955f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt @@ -9,10 +9,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.json.JSONIterator -import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault -import org.koitharu.kotatsu.parsers.util.json.getStringOrNull -import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.parsers.util.json.* class RestoreRepository(private val db: MangaDatabase) { @@ -95,7 +92,8 @@ class RestoreRepository(private val db: MangaDatabase) { updatedAt = json.getLong("updated_at"), chapterId = json.getLong("chapter_id"), page = json.getInt("page"), - scroll = json.getDouble("scroll").toFloat() + scroll = json.getDouble("scroll").toFloat(), + percent = json.getFloatOrDefault("percent", -1f), ) private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity( 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 2c74455f8..fbea719f5 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 @@ -6,8 +6,14 @@ 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.dao.MangaDao +import org.koitharu.kotatsu.core.db.dao.PreferencesDao +import org.koitharu.kotatsu.core.db.dao.TagsDao +import org.koitharu.kotatsu.core.db.dao.TrackLogsDao +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity +import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.migrations.* import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity @@ -27,7 +33,7 @@ import org.koitharu.kotatsu.tracker.data.TracksDao FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ], - version = 11, + version = 12, ) abstract class MangaDatabase : RoomDatabase() { @@ -67,6 +73,7 @@ fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( Migration8To9(), Migration9To10(), Migration10To11(), + Migration11To12(), ).addCallback( DatabasePrePopulateCallback(context.resources) ).build() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt new file mode 100644 index 000000000..923c1b401 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration11To12.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration11To12 : Migration(11, 12) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE history ADD COLUMN `percent` REAL NOT NULL DEFAULT -1") + database.execSQL("ALTER TABLE bookmarks ADD COLUMN `percent` REAL NOT NULL DEFAULT -1") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt index 95f736f2b..9ac085183 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaHistory.kt @@ -11,4 +11,5 @@ data class MangaHistory( val chapterId: Long, val page: Int, val scroll: Int, + val percent: Float, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt index c03300b99..0e5624559 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/EntityMapping.kt @@ -1,12 +1,13 @@ package org.koitharu.kotatsu.history.data -import java.util.* import org.koitharu.kotatsu.core.model.MangaHistory +import java.util.* fun HistoryEntity.toMangaHistory() = MangaHistory( createdAt = Date(createdAt), updatedAt = Date(updatedAt), chapterId = chapterId, page = page, - scroll = scroll.toInt() + scroll = scroll.toInt(), + percent = percent, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt index 24a487c60..181499fa4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt @@ -13,16 +13,17 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity entity = MangaEntity::class, parentColumns = ["manga_id"], childColumns = ["manga_id"], - onDelete = ForeignKey.CASCADE + onDelete = ForeignKey.CASCADE, ) ] ) class HistoryEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val mangaId: Long, - @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), + @ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "updated_at") val updatedAt: Long, @ColumnInfo(name = "chapter_id") val chapterId: Long, @ColumnInfo(name = "page") val page: Int, @ColumnInfo(name = "scroll") val scroll: Float, + @ColumnInfo(name = "percent") val percent: Float, ) \ 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 4519b60e4..fa043ad14 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 @@ -59,7 +59,7 @@ class HistoryRepository( .distinctUntilChanged() } - suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) { + suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float) { if (manga.isNsfw && settings.isHistoryExcludeNsfw) { return } @@ -75,6 +75,7 @@ class HistoryRepository( chapterId = chapterId, page = page, scroll = scroll.toFloat(), // we migrate to int, but decide to not update database + percent = percent, ) ) trackingRepository.syncWithHistory(manga, chapterId) 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 931461de0..39ec6946a 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 @@ -6,7 +6,6 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.R @@ -32,6 +31,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.ext.processLifecycleScope +import java.util.* private const val BOUNDS_PAGE_OFFSET = 2 private const val PAGES_TRIM_THRESHOLD = 120 @@ -135,13 +135,16 @@ class ReaderViewModel( } } + // TODO check performance fun saveCurrentState(state: ReaderState? = null) { if (state != null) { currentState.value = state } + val readerState = state ?: currentState.value ?: return historyRepository.saveStateAsync( - mangaData.value ?: return, - state ?: currentState.value ?: return + manga = mangaData.value ?: return, + state = readerState, + percent = computePercent(readerState.chapterId, readerState.page) ) } @@ -223,7 +226,7 @@ class ReaderViewModel( if (bookmarkJob?.isActive == true) { return } - bookmarkJob = launchJob { + bookmarkJob = launchJob(Dispatchers.Default) { loadingJob?.join() val state = checkNotNull(currentState.value) val page = checkNotNull(getCurrentPage()) { "Page not found" } @@ -235,9 +238,10 @@ class ReaderViewModel( scroll = state.scroll, imageUrl = page.preview ?: pageLoader.getPageUrl(page), createdAt = Date(), + percent = computePercent(state.chapterId, state.page), ) bookmarksRepository.addBookmark(bookmark) - onShowToast.call(R.string.bookmark_added) + onShowToast.postCall(R.string.bookmark_added) } } @@ -279,7 +283,8 @@ class ReaderViewModel( val pages = loadChapter(requireNotNull(currentState.value).chapterId) // save state currentState.value?.let { - historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll) + val percent = computePercent(it.chapterId, it.page) + historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) shortcutsRepository.updateShortcuts() } @@ -364,20 +369,32 @@ class ReaderViewModel( it.printStackTraceDebug() }.getOrDefault(defaultMode) } + + private fun computePercent(chapterId: Long, pageIndex: Int): Float { + val chapters = manga?.chapters ?: return -1f + val chaptersCount = chapters.size + val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } + val pages = content.value?.pages ?: return -1f + val pagesCount = pages.count { x -> x.chapterId == chapterId } + val chapterPercent = (chapterIndex + 1) / chaptersCount.toFloat() + val pagePercent = (pageIndex + 1) / pagesCount.toFloat() + return pagePercent * chapterPercent // FIXME + } } /** * 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 { +private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job { return processLifecycleScope.launch(Dispatchers.Default) { runCatching { addOrUpdate( manga = manga, chapterId = state.chapterId, page = state.page, - scroll = state.scroll + scroll = state.scroll, + percent = percent, ) }.onFailure { it.printStackTraceDebug() From 04dd8003f7078ae374f3c820ad04d211388555ce Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 28 Jun 2022 09:07:03 +0300 Subject: [PATCH 2/6] Show reading progress indicators in lists --- .../kotatsu/favourites/FavouritesModule.kt | 4 +- .../ui/list/FavouritesListViewModel.kt | 10 +- .../koitharu/kotatsu/history/HistoryModule.kt | 2 +- .../kotatsu/history/data/HistoryDao.kt | 18 ++- .../history/domain/HistoryRepository.kt | 6 + .../history/ui/HistoryListViewModel.kt | 10 +- .../ui/util/ReadingProgressDrawable.kt | 129 ++++++++++++++++++ .../history/ui/util/ReadingProgressView.kt | 114 ++++++++++++++++ ...untersProvider.kt => ListExtraProvider.kt} | 4 +- .../list/ui/adapter/MangaGridItemAD.kt | 5 +- .../list/ui/adapter/MangaListAdapter.kt | 13 +- .../ui/adapter/MangaListDetailedItemAD.kt | 5 +- .../list/ui/model/ListModelConversionExt.kt | 48 +++---- .../kotatsu/list/ui/model/MangaGridModel.kt | 7 +- .../kotatsu/list/ui/model/MangaItemModel.kt | 4 + .../list/ui/model/MangaListDetailedModel.kt | 7 +- .../kotatsu/list/ui/model/MangaListModel.kt | 7 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 3 + app/src/main/res/layout/item_manga_grid.xml | 29 ++-- .../res/layout/item_manga_list_details.xml | 27 +++- app/src/main/res/values/attrs.xml | 17 +++ app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/styles.xml | 11 ++ 23 files changed, 414 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt rename app/src/main/java/org/koitharu/kotatsu/list/domain/{CountersProvider.kt => ListExtraProvider.kt} (52%) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 6d880abb9..f3bc159a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -11,10 +11,10 @@ import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel val favouritesModule get() = module { - factory { FavouritesRepository(get(), get()) } + single { FavouritesRepository(get(), get()) } viewModel { categoryId -> - FavouritesListViewModel(categoryId.get(), get(), get(), get()) + FavouritesListViewModel(categoryId.get(), get(), get(), get(), get()) } viewModel { FavouritesCategoriesViewModel(get(), get()) } viewModel { manga -> 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 c2e8c00a1..6942925c0 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 @@ -11,7 +11,8 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID -import org.koitharu.kotatsu.list.domain.CountersProvider +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.LoadingState @@ -25,8 +26,9 @@ class FavouritesListViewModel( private val categoryId: Long, private val repository: FavouritesRepository, private val trackingRepository: TrackingRepository, + private val historyRepository: HistoryRepository, settings: AppSettings, -) : MangaListViewModel(settings), CountersProvider { +) : MangaListViewModel(settings), ListExtraProvider { var sortOrder: LiveData = if (categoryId == NO_ID) { MutableLiveData(null) @@ -92,4 +94,8 @@ class FavouritesListViewModel( override suspend fun getCounter(mangaId: Long): Int { return trackingRepository.getNewChaptersCount(mangaId) } + + override suspend fun getProgress(mangaId: Long): Float { + return historyRepository.getProgress(mangaId) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index 246cb3a5f..74fb0ef40 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel val historyModule get() = module { - factory { HistoryRepository(get(), get(), get()) } + single { HistoryRepository(get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get(), get()) } } \ 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 f52c5db6d..f27a0af0e 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 @@ -45,26 +45,36 @@ abstract class HistoryDao { @Query("SELECT COUNT(*) FROM history") abstract fun observeCount(): Flow + @Query("SELECT percent FROM history WHERE manga_id = :id") + abstract fun findProgress(id: Long): Float? + @Query("DELETE FROM history") abstract suspend fun clear() @Insert(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun insert(entity: HistoryEntity): Long - @Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId") + @Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt WHERE manga_id = :mangaId") abstract suspend fun update( mangaId: Long, page: Int, chapterId: Long, scroll: Float, - updatedAt: Long + percent: Float, + updatedAt: Long, ): Int @Query("DELETE FROM history WHERE manga_id = :mangaId") abstract suspend fun delete(mangaId: Long) - suspend fun update(entity: HistoryEntity) = - update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt) + suspend fun update(entity: HistoryEntity) = update( + mangaId = entity.mangaId, + page = entity.page, + chapterId = entity.chapterId, + scroll = entity.scroll, + percent = entity.percent, + updatedAt = entity.updatedAt + ) @Transaction open suspend fun upsert(entity: HistoryEntity): Boolean { 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 fa043ad14..61f49d6d9 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 @@ -16,6 +16,8 @@ import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.mapItems +const val PROGRESS_NONE = -1f + class HistoryRepository( private val db: MangaDatabase, private val trackingRepository: TrackingRepository, @@ -86,6 +88,10 @@ class HistoryRepository( return db.historyDao.find(manga.id)?.toMangaHistory() } + suspend fun getProgress(mangaId: Long): Float { + return db.historyDao.findProgress(mangaId) ?: PROGRESS_NONE + } + suspend fun clear() { db.historyDao.clear() } 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 1768c2a5f..231c89817 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,8 +2,6 @@ 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.catch import kotlinx.coroutines.flow.combine @@ -26,6 +24,8 @@ 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 +import java.util.* +import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, @@ -112,9 +112,9 @@ class HistoryListViewModel( } val counter = trackingRepository.getNewChaptersCount(manga.id) result += when (mode) { - ListMode.LIST -> manga.toListModel(counter) - ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter) - ListMode.GRID -> manga.toGridModel(counter) + ListMode.LIST -> manga.toListModel(counter, history.percent) + ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, history.percent) + ListMode.GRID -> manga.toGridModel(counter, history.percent) } } return result diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt new file mode 100644 index 000000000..531615c03 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt @@ -0,0 +1,129 @@ +package org.koitharu.kotatsu.history.ui.util + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import androidx.annotation.StyleRes +import androidx.core.graphics.ColorUtils +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import kotlin.math.roundToInt + +class ReadingProgressDrawable( + context: Context, + @StyleRes styleResId: Int, +) : Drawable() { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val lineColor: Int + private val outlineColor: Int + private val backgroundColor: Int + private val textColor: Int + private val textPattern = context.getString(R.string.percent_string_pattern) + private val textBounds = Rect() + private val hasBackground: Boolean + private val hasOutline: Boolean + private val hasText: Boolean + private val desiredHeight: Int + private val desiredWidth: Int + private val autoFitTextSize: Boolean + + var progress: Float = PROGRESS_NONE + set(value) { + field = value + text = textPattern.format((value * 100f).roundToInt().toString()) + paint.getTextBounds(text, 0, text.length, textBounds) + invalidateSelf() + } + private var text = "" + + init { + val ta = context.obtainStyledAttributes(styleResId, R.styleable.ProgressDrawable) + desiredHeight = ta.getDimensionPixelSize(R.styleable.ProgressDrawable_android_height, -1) + desiredWidth = ta.getDimensionPixelSize(R.styleable.ProgressDrawable_android_width, -1) + autoFitTextSize = ta.getBoolean(R.styleable.ProgressDrawable_autoFitTextSize, false) + lineColor = ta.getColor(R.styleable.ProgressDrawable_android_strokeColor, Color.BLACK) + outlineColor = ta.getColor(R.styleable.ProgressDrawable_outlineColor, Color.TRANSPARENT) + backgroundColor = ColorUtils.setAlphaComponent( + ta.getColor(R.styleable.ProgressDrawable_android_fillColor, Color.TRANSPARENT), + (255 * ta.getFloat(R.styleable.ProgressDrawable_android_fillAlpha, 0f)).toInt(), + ) + textColor = ta.getColor(R.styleable.ProgressDrawable_android_textColor, lineColor) + paint.strokeCap = Paint.Cap.ROUND + paint.textAlign = Paint.Align.CENTER + paint.textSize = ta.getDimension(R.styleable.ProgressDrawable_android_textSize, paint.textSize) + paint.strokeWidth = ta.getDimension(R.styleable.ProgressDrawable_strokeWidth, 1f) + hasBackground = Color.alpha(backgroundColor) != 0 + hasOutline = Color.alpha(outlineColor) != 0 + hasText = Color.alpha(textColor) != 0 && paint.textSize > 0 + ta.recycle() + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + if (autoFitTextSize) { + val innerWidth = bounds.width() - (paint.strokeWidth * 2f) + paint.textSize = getTextSizeForWidth(innerWidth, "100%") + } + } + + override fun draw(canvas: Canvas) { + if (progress < 0f) { + return + } + val cx = bounds.exactCenterX() + val cy = bounds.exactCenterY() + val radius = minOf(bounds.width(), bounds.height()) / 2f + if (hasBackground) { + paint.style = Paint.Style.FILL + paint.color = backgroundColor + canvas.drawCircle(cx, cy, radius, paint) + } + val innerRadius = radius - paint.strokeWidth / 2f + paint.style = Paint.Style.STROKE + if (hasOutline) { + paint.color = outlineColor + canvas.drawCircle(cx, cy, innerRadius, paint) + } + paint.color = lineColor + canvas.drawArc( + cx - innerRadius, + cy - innerRadius, + cx + innerRadius, + cy + innerRadius, + -90f, + 360f * progress, + false, + paint, + ) + if (hasText) { + paint.style = Paint.Style.FILL + paint.color = textColor + val ty = bounds.height() / 2f + textBounds.height() / 2f - textBounds.bottom + canvas.drawText(text, cx, ty, paint) + } + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + @Suppress("DeprecatedCallableAddReplaceWith") + @Deprecated("Deprecated in Java") + override fun getOpacity() = PixelFormat.TRANSLUCENT + + override fun getIntrinsicHeight() = desiredHeight + + override fun getIntrinsicWidth() = desiredWidth + + private fun getTextSizeForWidth(width: Float, text: String): Float { + val testTextSize = 48f + paint.textSize = testTextSize + paint.getTextBounds(text, 0, text.length, textBounds) + return testTextSize * width / textBounds.width() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt new file mode 100644 index 000000000..2bb906c99 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressView.kt @@ -0,0 +1,114 @@ +package org.koitharu.kotatsu.history.ui.util + +import android.animation.Animator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Outline +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE + +class ReadingProgressView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { + + private var percentAnimator: ValueAnimator? = null + private val animationDuration = context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong() + + @StyleRes + private val drawableStyle: Int + + var percent: Float + get() = peekProgressDrawable()?.progress ?: PROGRESS_NONE + set(value) { + cancelAnimation() + getProgressDrawable().progress = value + } + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.ReadingProgressView, defStyleAttr, 0) + drawableStyle = ta.getResourceId(R.styleable.ReadingProgressView_progressStyle, R.style.ProgressDrawable) + ta.recycle() + outlineProvider = OutlineProvider() + if (isInEditMode) { + percent = 0.27f + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + percentAnimator?.run { + if (isRunning) end() + } + percentAnimator = null + } + + override fun onAnimationUpdate(animation: ValueAnimator) { + val p = animation.animatedValue as Float + getProgressDrawable().progress = p + } + + override fun onAnimationStart(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + if (percentAnimator === animation) { + percentAnimator = null + } + } + + override fun onAnimationCancel(animation: Animator?) = Unit + + override fun onAnimationRepeat(animation: Animator?) = Unit + + fun setPercent(value: Float, animate: Boolean) { + val currentDrawable = peekProgressDrawable() + if (!animate || currentDrawable == null || value == PROGRESS_NONE) { + percent = value + return + } + percentAnimator?.cancel() + percentAnimator = ValueAnimator.ofFloat( + currentDrawable.progress.coerceAtLeast(0f), + value + ).apply { + duration = animationDuration + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener(this@ReadingProgressView) + addListener(this@ReadingProgressView) + start() + } + } + + private fun cancelAnimation() { + percentAnimator?.cancel() + percentAnimator = null + } + + private fun peekProgressDrawable(): ReadingProgressDrawable? { + return background as? ReadingProgressDrawable + } + + private fun getProgressDrawable(): ReadingProgressDrawable { + var d = peekProgressDrawable() + if (d != null) { + return d + } + d = ReadingProgressDrawable(context, drawableStyle) + background = d + return d + } + + private class OutlineProvider : ViewOutlineProvider() { + + override fun getOutline(view: View, outline: Outline) { + outline.setOval(0, 0, view.width, view.height) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt similarity index 52% rename from app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt rename to app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt index e9594019c..7c547e0ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/domain/ListExtraProvider.kt @@ -1,6 +1,8 @@ package org.koitharu.kotatsu.list.domain -fun interface CountersProvider { +interface ListExtraProvider { suspend fun getCounter(mangaId: Long): Int + + suspend fun getProgress(mangaId: Long): Float } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index e4ad38d3e..8145e240e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -11,6 +11,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemMangaGridBinding +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.parsers.model.Manga @@ -43,8 +44,9 @@ fun mangaGridItemAD( } } - bind { + bind { payloads -> binding.textViewTitle.text = item.title + binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) imageRequest?.dispose() imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl) .referer(item.manga.publicUrl) @@ -60,6 +62,7 @@ fun mangaGridItemAD( onViewRecycled { itemView.clearBadge(badge) + binding.progressView.percent = PROGRESS_NONE badge = null imageRequest?.dispose() imageRequest = null diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 93f271c0c..6b69445c5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -54,9 +54,14 @@ class MangaListAdapter( override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? { return when (newItem) { - is MangaListModel, - is MangaGridModel, - is MangaListDetailedModel, + is MangaItemModel -> { + oldItem as MangaItemModel + if (oldItem.progress != newItem.progress) { + PAYLOAD_PROGRESS + } else { + Unit + } + } is CurrentFilterModel -> Unit else -> super.getChangePayload(oldItem, newItem) } @@ -77,5 +82,7 @@ class MangaListAdapter( const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_FILTER = 10 const val ITEM_TYPE_HEADER_FILTER = 11 + + val PAYLOAD_PROGRESS = Any() } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 74a67378f..583be4079 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -10,6 +10,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel import org.koitharu.kotatsu.parsers.model.Manga @@ -36,10 +37,11 @@ fun mangaListDetailedItemAD( clickListener.onItemLongClick(item.manga, it) } - bind { + bind { payloads -> imageRequest?.dispose() binding.textViewTitle.text = item.title binding.textViewSubtitle.textAndVisible = item.subtitle + binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) imageRequest = binding.imageViewCover.newImageRequest(item.coverUrl) .referer(item.manga.publicUrl) .placeholder(R.drawable.ic_placeholder) @@ -56,6 +58,7 @@ fun mangaListDetailedItemAD( onViewRecycled { itemView.clearBadge(badge) + binding.progressView.percent = PROGRESS_NONE badge = null imageRequest?.dispose() imageRequest = null diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index cda127e73..b642a248b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -4,21 +4,23 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.list.domain.CountersProvider +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.ifZero -fun Manga.toListModel(counter: Int) = MangaListModel( +fun Manga.toListModel(counter: Int, progress: Float) = MangaListModel( id = id, title = title, subtitle = tags.joinToString(", ") { it.title }, coverUrl = coverUrl, manga = this, counter = counter, + progress = progress, ) -fun Manga.toListDetailedModel(counter: Int) = MangaListDetailedModel( +fun Manga.toListDetailedModel(counter: Int, progress: Float) = MangaListDetailedModel( id = id, title = title, subtitle = altTitle, @@ -27,50 +29,48 @@ fun Manga.toListDetailedModel(counter: Int) = MangaListDetailedModel( coverUrl = coverUrl, manga = this, counter = counter, + progress = progress, ) -fun Manga.toGridModel(counter: Int) = MangaGridModel( +fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel( id = id, title = title, coverUrl = coverUrl, manga = this, counter = counter, + progress = progress, ) suspend fun List.toUi( mode: ListMode, - countersProvider: CountersProvider, + extraProvider: ListExtraProvider, ): List = when (mode) { - ListMode.LIST -> map { it.toListModel(countersProvider.getCounter(it.id)) } - ListMode.DETAILED_LIST -> map { it.toListDetailedModel(countersProvider.getCounter(it.id)) } - ListMode.GRID -> map { it.toGridModel(countersProvider.getCounter(it.id)) } -} - -suspend fun > List.toUi( - destination: C, - mode: ListMode, - countersProvider: CountersProvider, -): C = when (mode) { - ListMode.LIST -> mapTo(destination) { it.toListModel(countersProvider.getCounter(it.id)) } - ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(countersProvider.getCounter(it.id)) } - ListMode.GRID -> mapTo(destination) { it.toGridModel(countersProvider.getCounter(it.id)) } + ListMode.LIST -> map { + it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) + } + ListMode.DETAILED_LIST -> map { + it.toListDetailedModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) + } + ListMode.GRID -> map { + it.toGridModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) + } } fun List.toUi( mode: ListMode, ): List = when (mode) { - ListMode.LIST -> map { it.toListModel(0) } - ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0) } - ListMode.GRID -> map { it.toGridModel(0) } + ListMode.LIST -> map { it.toListModel(0, PROGRESS_NONE) } + ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0, PROGRESS_NONE) } + ListMode.GRID -> map { it.toGridModel(0, PROGRESS_NONE) } } fun > List.toUi( destination: C, mode: ListMode, ): C = when (mode) { - ListMode.LIST -> mapTo(destination) { it.toListModel(0) } - ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0) } - ListMode.GRID -> mapTo(destination) { it.toGridModel(0) } + ListMode.LIST -> mapTo(destination) { it.toListModel(0, PROGRESS_NONE) } + ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0, PROGRESS_NONE) } + ListMode.GRID -> mapTo(destination) { it.toGridModel(0, PROGRESS_NONE) } } fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt index 9575d82fd..97a414879 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt @@ -4,8 +4,9 @@ import org.koitharu.kotatsu.parsers.model.Manga data class MangaGridModel( override val id: Long, - val title: String, - val coverUrl: String, + override val title: String, + override val coverUrl: String, override val manga: Manga, - val counter: Int, + override val counter: Int, + override val progress: Float, ) : MangaItemModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt index 64a4f66ae..ce5145d26 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaItemModel.kt @@ -6,4 +6,8 @@ sealed interface MangaItemModel : ListModel { val id: Long val manga: Manga + val title: String + val coverUrl: String + val counter: Int + val progress: Float } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt index c1cedeb24..f9957f345 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt @@ -4,11 +4,12 @@ import org.koitharu.kotatsu.parsers.model.Manga data class MangaListDetailedModel( override val id: Long, - val title: String, + override val title: String, val subtitle: String?, val tags: String, - val coverUrl: String, + override val coverUrl: String, val rating: String?, override val manga: Manga, - val counter: Int, + override val counter: Int, + override val progress: Float, ) : MangaItemModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt index 7533858eb..71ac5743c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt @@ -4,9 +4,10 @@ import org.koitharu.kotatsu.parsers.model.Manga data class MangaListModel( override val id: Long, - val title: String, + override val title: String, val subtitle: String, - val coverUrl: String, + override val coverUrl: String, override val manga: Manga, - val counter: Int, + override val counter: Int, + override val progress: Float, ) : MangaItemModel \ 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 39ec6946a..1fe633456 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 @@ -376,6 +376,9 @@ class ReaderViewModel( val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } val pages = content.value?.pages ?: return -1f val pagesCount = pages.count { x -> x.chapterId == chapterId } + if (chaptersCount == 0 || pagesCount == 0) { + return -1f + } val chapterPercent = (chapterIndex + 1) / chaptersCount.toFloat() val pagePercent = (pageIndex + 1) / pagesCount.toFloat() return pagePercent * chapterPercent // FIXME diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index 5174b647a..ef4ac491d 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -13,15 +13,28 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + android:layout_height="wrap_content"> + + + + + + - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + \ 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 d563b55f8..622bdf251 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -26,6 +26,8 @@ 10dp 116dp 84dp + 4dp + 10dp 124dp 4dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b4f3f77cf..b3f122cec 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -173,4 +173,15 @@ @layout/preference_widget_material_switch + + + + From f2ea1cde46e0d013690351a02282a0429a076e1e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 28 Jun 2022 12:10:39 +0300 Subject: [PATCH 3/6] Reading progress indicator on details screen --- .../koitharu/kotatsu/details/ui/DetailsFragment.kt | 2 ++ .../history/ui/util/ReadingProgressDrawable.kt | 3 +-- .../koitharu/kotatsu/reader/ui/ReaderViewModel.kt | 13 +++++++------ app/src/main/res/layout-w600dp/fragment_details.xml | 8 ++++++++ app/src/main/res/layout/fragment_details.xml | 8 ++++++++ 5 files changed, 26 insertions(+), 8 deletions(-) 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 dfc427d99..acae5f2c4 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 @@ -32,6 +32,7 @@ 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 +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource @@ -176,6 +177,7 @@ class DetailsFragment : setIconResource(R.drawable.ic_play) } } + binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true) } private fun onFavouriteChanged(isFavourite: Boolean) { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt index 531615c03..3500d9398 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt @@ -7,7 +7,6 @@ import androidx.annotation.StyleRes import androidx.core.graphics.ColorUtils import org.koitharu.kotatsu.R import org.koitharu.kotatsu.history.domain.PROGRESS_NONE -import kotlin.math.roundToInt class ReadingProgressDrawable( context: Context, @@ -31,7 +30,7 @@ class ReadingProgressDrawable( var progress: Float = PROGRESS_NONE set(value) { field = value - text = textPattern.format((value * 100f).roundToInt().toString()) + text = textPattern.format((value * 100f).toInt().toString()) paint.getTextBounds(text, 0, text.length, textBounds) invalidateSelf() } 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 1fe633456..3ef07926b 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 @@ -20,6 +20,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage @@ -144,7 +145,7 @@ class ReaderViewModel( historyRepository.saveStateAsync( manga = mangaData.value ?: return, state = readerState, - percent = computePercent(readerState.chapterId, readerState.page) + percent = computePercent(readerState.chapterId, readerState.page), ) } @@ -371,17 +372,17 @@ class ReaderViewModel( } private fun computePercent(chapterId: Long, pageIndex: Int): Float { - val chapters = manga?.chapters ?: return -1f + val chapters = manga?.chapters ?: return PROGRESS_NONE val chaptersCount = chapters.size val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } - val pages = content.value?.pages ?: return -1f + val pages = content.value?.pages ?: return PROGRESS_NONE val pagesCount = pages.count { x -> x.chapterId == chapterId } if (chaptersCount == 0 || pagesCount == 0) { - return -1f + return PROGRESS_NONE } - val chapterPercent = (chapterIndex + 1) / chaptersCount.toFloat() val pagePercent = (pageIndex + 1) / pagesCount.toFloat() - return pagePercent * chapterPercent // FIXME + val ppc = 1f / chaptersCount + return ppc * chapterIndex + ppc * pagePercent } } diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml index c691b4ea1..16eab6a18 100644 --- a/app/src/main/res/layout-w600dp/fragment_details.xml +++ b/app/src/main/res/layout-w600dp/fragment_details.xml @@ -31,6 +31,14 @@ tools:background="@tools:sample/backgrounds/scenic" tools:ignore="ContentDescription,UnusedAttribute" /> + + + + Date: Tue, 28 Jun 2022 12:23:27 +0300 Subject: [PATCH 4/6] Option to disable reading progress indicators --- .../kotatsu/core/prefs/AppSettings.kt | 6 ++- .../ui/list/FavouritesListViewModel.kt | 9 +++- .../history/ui/HistoryListViewModel.kt | 13 +++-- app/src/main/res/values/strings.xml | 5 ++ app/src/main/res/xml/pref_history.xml | 49 ++++++++++++------- 5 files changed, 55 insertions(+), 27 deletions(-) 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 5dd49a44b..2c75acacb 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 @@ -104,10 +104,13 @@ class AppSettings(context: Context) { val isReaderModeDetectionEnabled: Boolean get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true) - var historyGrouping: Boolean + var isHistoryGroupingEnabled: Boolean get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true) set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) } + val isReadingIndicatorsEnabled: Boolean + get() = prefs.getBoolean(KEY_READING_INDICATORS, true) + val isHistoryExcludeNsfw: Boolean get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false) @@ -303,6 +306,7 @@ class AppSettings(context: Context) { const val KEY_BACKUP = "backup" const val KEY_RESTORE = "restore" const val KEY_HISTORY_GROUPING = "history_grouping" + const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw" const val KEY_PAGES_NUMBERS = "pages_numbers" 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 6942925c0..0c8b87c2a 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 @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -27,7 +28,7 @@ class FavouritesListViewModel( private val repository: FavouritesRepository, private val trackingRepository: TrackingRepository, private val historyRepository: HistoryRepository, - settings: AppSettings, + private val settings: AppSettings, ) : MangaListViewModel(settings), ListExtraProvider { var sortOrder: LiveData = if (categoryId == NO_ID) { @@ -96,6 +97,10 @@ class FavouritesListViewModel( } override suspend fun getProgress(mangaId: Long): Float { - return historyRepository.getProgress(mangaId) + return if (settings.isReadingIndicatorsEnabled) { + historyRepository.getProgress(mangaId) + } else { + PROGRESS_NONE + } } } \ No newline at end of file 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 231c89817..5af2747af 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 @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory +import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.tracker.domain.TrackingRepository @@ -37,7 +38,7 @@ class HistoryListViewModel( val isGroupingEnabled = MutableLiveData() val onItemsRemoved = SingleLiveEvent() - private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping } + private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled } .onEach { isGroupingEnabled.postValue(it) } override val content = combine( @@ -89,7 +90,7 @@ class HistoryListViewModel( } fun setGrouping(isGroupingEnabled: Boolean) { - settings.historyGrouping = isGroupingEnabled + settings.isHistoryGroupingEnabled = isGroupingEnabled } private suspend fun mapList( @@ -98,6 +99,7 @@ class HistoryListViewModel( mode: ListMode ): List { val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1) + val showPercent = settings.isReadingIndicatorsEnabled var prevDate: DateTimeAgo? = null if (!grouped) { result += ListHeader(null, R.string.history, null) @@ -111,10 +113,11 @@ class HistoryListViewModel( prevDate = date } val counter = trackingRepository.getNewChaptersCount(manga.id) + val percent = if (showPercent) history.percent else PROGRESS_NONE result += when (mode) { - ListMode.LIST -> manga.toListModel(counter, history.percent) - ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, history.percent) - ListMode.GRID -> manga.toGridModel(counter, history.percent) + ListMode.LIST -> manga.toListModel(counter, percent) + ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent) + ListMode.GRID -> manga.toGridModel(counter, percent) } } return result diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79ec942ea..a72a39ba9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,4 +304,9 @@ Use fingerprint if available Manga from your favourites Your recently read manga + Show reading progress indicators + Data deletion + Show percentage read in history and favourites + Manga marked as NSFW will never added to the history and your progress will not be saved + Can help in case of some issues. All authorizations will be invalidated \ No newline at end of file diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index 6b7d82c50..38000e3f4 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -1,30 +1,39 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + - + + + + + + android:title="@string/clear_thumbs_cache" + app:allowDividerAbove="true" /> + + - - \ No newline at end of file From d9459dc8fa1fd455b5484ee48902c7d73c5ab9b3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 28 Jun 2022 12:44:59 +0300 Subject: [PATCH 5/6] Ui adjustments --- .../kotatsu/history/ui/util/ReadingProgressDrawable.kt | 7 +++++-- app/src/main/res/values/styles.xml | 3 ++- app/src/main/res/xml/pref_history.xml | 6 ++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt index 3500d9398..1e985b9f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt @@ -20,6 +20,7 @@ class ReadingProgressDrawable( private val textColor: Int private val textPattern = context.getString(R.string.percent_string_pattern) private val textBounds = Rect() + private val tempRect = Rect() private val hasBackground: Boolean private val hasOutline: Boolean private val hasText: Boolean @@ -63,6 +64,8 @@ class ReadingProgressDrawable( if (autoFitTextSize) { val innerWidth = bounds.width() - (paint.strokeWidth * 2f) paint.textSize = getTextSizeForWidth(innerWidth, "100%") + paint.getTextBounds(text, 0, text.length, textBounds) + invalidateSelf() } } @@ -122,7 +125,7 @@ class ReadingProgressDrawable( private fun getTextSizeForWidth(width: Float, text: String): Float { val testTextSize = 48f paint.textSize = testTextSize - paint.getTextBounds(text, 0, text.length, textBounds) - return testTextSize * width / textBounds.width() + paint.getTextBounds(text, 0, text.length, tempRect) + return testTextSize * width / tempRect.width() } } \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b3f122cec..ba37673c2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -182,6 +182,7 @@ ?android:textColorPrimary 3dp 9sp + true - + diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index 38000e3f4..e046bf8fa 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -32,8 +32,7 @@ android:key="thumbs_cache_clear" android:persistent="false" android:summary="@string/computing_" - android:title="@string/clear_thumbs_cache" - app:allowDividerAbove="true" /> + android:title="@string/clear_thumbs_cache" /> + android:title="@string/clear_cookies" /> From 1044d7a8d1388ba9ab645ecd66d659ac876ba969 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 1 Jul 2022 15:32:47 +0300 Subject: [PATCH 6/6] Show checkmark on reading indicator if full --- .../ui/util/ReadingProgressDrawable.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt index 1e985b9f1..9fd4542ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/util/ReadingProgressDrawable.kt @@ -4,9 +4,11 @@ import android.content.Context import android.graphics.* import android.graphics.drawable.Drawable import androidx.annotation.StyleRes +import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.ColorUtils import org.koitharu.kotatsu.R import org.koitharu.kotatsu.history.domain.PROGRESS_NONE +import kotlin.math.roundToInt class ReadingProgressDrawable( context: Context, @@ -14,6 +16,7 @@ class ReadingProgressDrawable( ) : Drawable() { private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val checkDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_check) private val lineColor: Int private val outlineColor: Int private val backgroundColor: Int @@ -53,10 +56,11 @@ class ReadingProgressDrawable( paint.textAlign = Paint.Align.CENTER paint.textSize = ta.getDimension(R.styleable.ProgressDrawable_android_textSize, paint.textSize) paint.strokeWidth = ta.getDimension(R.styleable.ProgressDrawable_strokeWidth, 1f) + ta.recycle() hasBackground = Color.alpha(backgroundColor) != 0 hasOutline = Color.alpha(outlineColor) != 0 hasText = Color.alpha(textColor) != 0 && paint.textSize > 0 - ta.recycle() + checkDrawable?.setTint(textColor) } override fun onBoundsChange(bounds: Rect) { @@ -99,10 +103,17 @@ class ReadingProgressDrawable( paint, ) if (hasText) { - paint.style = Paint.Style.FILL - paint.color = textColor - val ty = bounds.height() / 2f + textBounds.height() / 2f - textBounds.bottom - canvas.drawText(text, cx, ty, paint) + if (checkDrawable != null && progress >= 1f - Math.ulp(progress)) { + tempRect.set(bounds) + tempRect *= 0.6 + checkDrawable.bounds = tempRect + checkDrawable.draw(canvas) + } else { + paint.style = Paint.Style.FILL + paint.color = textColor + val ty = bounds.height() / 2f + textBounds.height() / 2f - textBounds.bottom + canvas.drawText(text, cx, ty, paint) + } } } @@ -128,4 +139,13 @@ class ReadingProgressDrawable( paint.getTextBounds(text, 0, text.length, tempRect) return testTextSize * width / tempRect.width() } + + private operator fun Rect.timesAssign(factor: Double) { + val newWidth = (width() * factor).roundToInt() + val newHeight = (height() * factor).roundToInt() + inset( + (width() - newWidth) / 2, + (height() - newHeight) / 2, + ) + } } \ No newline at end of file