Merge branch 'feature/percent' into devel

This commit is contained in:
Koitharu
2022-07-02 14:51:50 +03:00
38 changed files with 557 additions and 110 deletions

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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(

View File

@@ -21,5 +21,7 @@ class Migration11To12 : Migration(11, 12) {
)
""".trimIndent()
)
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")
}
}

View File

@@ -11,4 +11,5 @@ data class MangaHistory(
val chapterId: Long,
val page: Int,
val scroll: Int,
val percent: Float,
) : Parcelable

View File

@@ -105,10 +105,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)
@@ -296,6 +299,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"

View File

@@ -34,6 +34,7 @@ import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet
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
@@ -181,6 +182,7 @@ class DetailsFragment :
setIconResource(R.drawable.ic_play)
}
}
binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
}
private fun onFavouriteChanged(isFavourite: Boolean) {

View File

@@ -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 ->

View File

@@ -11,7 +11,9 @@ 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.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
@@ -25,8 +27,9 @@ class FavouritesListViewModel(
private val categoryId: Long,
private val repository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
settings: AppSettings,
) : MangaListViewModel(settings), CountersProvider {
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
) : MangaListViewModel(settings), ListExtraProvider {
var sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
MutableLiveData(null)
@@ -92,4 +95,12 @@ class FavouritesListViewModel(
override suspend fun getCounter(mangaId: Long): Int {
return trackingRepository.getNewChaptersCount(mangaId)
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
}

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel
val historyModule
get() = module {
factory { HistoryRepository(get(), get(), get(), getAll()) }
single { HistoryRepository(get(), get(), get(), getAll()) }
viewModel { HistoryListViewModel(get(), get(), get(), get()) }
}

View File

@@ -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,
)

View File

@@ -45,26 +45,36 @@ abstract class HistoryDao {
@Query("SELECT COUNT(*) FROM history")
abstract fun observeCount(): Flow<Int>
@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 {

View File

@@ -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,
)

View File

@@ -18,6 +18,8 @@ import org.koitharu.kotatsu.scrobbling.domain.tryScrobble
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,
@@ -62,7 +64,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
}
@@ -78,6 +80,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)
@@ -92,6 +95,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()
}

View File

@@ -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
@@ -19,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
@@ -26,6 +25,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,
@@ -37,7 +38,7 @@ class HistoryListViewModel(
val isGroupingEnabled = MutableLiveData<Boolean>()
val onItemsRemoved = SingleLiveEvent<ReversibleHandle>()
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<ListModel> {
val result = ArrayList<ListModel>(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)
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter)
ListMode.GRID -> manga.toGridModel(counter)
ListMode.LIST -> manga.toListModel(counter, percent)
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent)
ListMode.GRID -> manga.toGridModel(counter, percent)
}
}
return result

View File

@@ -0,0 +1,151 @@
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.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,
@StyleRes styleResId: Int,
) : 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
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
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).toInt().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)
ta.recycle()
hasBackground = Color.alpha(backgroundColor) != 0
hasOutline = Color.alpha(outlineColor) != 0
hasText = Color.alpha(textColor) != 0 && paint.textSize > 0
checkDrawable?.setTint(textColor)
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
if (autoFitTextSize) {
val innerWidth = bounds.width() - (paint.strokeWidth * 2f)
paint.textSize = getTextSizeForWidth(innerWidth, "100%")
paint.getTextBounds(text, 0, text.length, textBounds)
invalidateSelf()
}
}
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) {
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)
}
}
}
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, 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,
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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<Manga>.toUi(
mode: ListMode,
countersProvider: CountersProvider,
extraProvider: ListExtraProvider,
): List<MangaItemModel> = 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 <C : MutableCollection<ListModel>> List<Manga>.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<Manga>.toUi(
mode: ListMode,
): List<MangaItemModel> = 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 <C : MutableCollection<ListModel>> List<Manga>.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(

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -21,6 +21,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
@@ -137,13 +138,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),
)
}
@@ -225,7 +229,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" }
@@ -237,9 +241,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)
}
}
@@ -282,7 +287,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()
}
@@ -367,20 +373,35 @@ class ReaderViewModel(
it.printStackTraceDebug()
}.getOrDefault(defaultMode)
}
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
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 PROGRESS_NONE
val pagesCount = pages.count { x -> x.chapterId == chapterId }
if (chaptersCount == 0 || pagesCount == 0) {
return PROGRESS_NONE
}
val pagePercent = (pageIndex + 1) / pagesCount.toFloat()
val ppc = 1f / chaptersCount
return ppc * chapterIndex + ppc * pagePercent
}
}
/**
* This function is not a member of the ReaderViewModel
* because it should work independently of the ViewModel's lifecycle.
*/
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState): 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()

View File

@@ -31,6 +31,14 @@
tools:background="@tools:sample/backgrounds/scenic"
tools:ignore="ContentDescription,UnusedAttribute" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
<LinearLayout
android:id="@+id/layout_titles"
android:layout_width="0dp"

View File

@@ -31,6 +31,14 @@
tools:background="@tools:sample/backgrounds/scenic"
tools:ignore="ContentDescription,UnusedAttribute" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="4dp"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"

View File

@@ -13,15 +13,28 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic[3]" />
android:layout_height="wrap_content">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic[3]" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom|end"
android:layout_margin="4dp" />
</FrameLayout>
<TextView
android:id="@+id/textView_title"

View File

@@ -12,14 +12,27 @@
android:layout_height="match_parent"
android:orientation="horizontal">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@tools:sample/backgrounds/scenic" />
android:layout_height="match_parent">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@tools:sample/backgrounds/scenic" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom|end"
android:layout_margin="4dp" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"

View File

@@ -35,4 +35,21 @@
<attr name="android:insetRight" />
</declare-styleable>
<declare-styleable name="ProgressDrawable">
<attr name="strokeWidth" />
<attr name="android:strokeColor" />
<attr name="android:textSize" />
<attr name="android:textColor" />
<attr name="android:fillColor" />
<attr name="android:fillAlpha" />
<attr name="android:width" />
<attr name="android:height" />
<attr name="outlineColor" format="color" />
<attr name="autoFitTextSize" format="boolean" />
</declare-styleable>
<declare-styleable name="ReadingProgressView">
<attr name="progressStyle" format="reference" />
</declare-styleable>
</resources>

View File

@@ -26,6 +26,8 @@
<dimen name="toolbar_button_margin">10dp</dimen>
<dimen name="widget_cover_height">116dp</dimen>
<dimen name="widget_cover_width">84dp</dimen>
<dimen name="reading_progress_stroke">4dp</dimen>
<dimen name="reading_progress_text_size">10dp</dimen>
<dimen name="search_suggestions_manga_height">124dp</dimen>
<dimen name="search_suggestions_manga_spacing">4dp</dimen>

View File

@@ -314,4 +314,9 @@
<string name="appwidget_shelf_description">Manga from your favourites</string>
<string name="appwidget_recent_description">Your recently read manga</string>
<string name="report">Report</string>
<string name="show_reading_indicators">Show reading progress indicators</string>
<string name="data_deletion">Data deletion</string>
<string name="show_reading_indicators_summary">Show percentage read in history and favourites</string>
<string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string>
<string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string>
</resources>

View File

@@ -173,4 +173,16 @@
<item name="android:widgetLayout">@layout/preference_widget_material_switch</item>
</style>
<!-- Progress drawable -->
<style name="ProgressDrawable">
<item name="android:fillAlpha">0.8</item>
<item name="android:fillColor">?android:colorBackground</item>
<item name="android:strokeColor">?colorPrimaryDark</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="strokeWidth">3dp</item>
<item name="android:textSize">9sp</item>
<item name="autoFitTextSize">true</item>
</style>
</resources>

View File

@@ -2,23 +2,39 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="search_history_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_search_history" />
<Preference
android:key="updates_feed_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_updates_feed" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="reading_indicators"
android:summary="@string/show_reading_indicators_summary"
android:title="@string/show_reading_indicators" />
<SwitchPreferenceCompat
android:key="history_exclude_nsfw"
android:summary="@string/exclude_nsfw_from_history_summary"
android:title="@string/exclude_nsfw_from_history" />
<PreferenceCategory android:title="@string/cache">
<PreferenceCategory android:title="@string/tracking">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment"
android:key="shikimori"
android:title="@string/shikimori" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/data_deletion">
<Preference
android:key="search_history_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_search_history" />
<Preference
android:key="updates_feed_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_updates_feed" />
<Preference
android:key="thumbs_cache_clear"
@@ -32,6 +48,12 @@
android:summary="@string/computing_"
android:title="@string/clear_pages_cache" />
<Preference
android:key="cookies_clear"
android:persistent="false"
android:summary="@string/clear_cookies_summary"
android:title="@string/clear_cookies" />
</PreferenceCategory>
<Preference
@@ -39,13 +61,4 @@
android:persistent="false"
android:title="@string/clear_cookies" />
<PreferenceCategory android:title="@string/tracking">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment"
android:key="shikimori"
android:title="@string/shikimori" />
</PreferenceCategory>
</PreferenceScreen>