Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ fun StatsScreenContent(
onDateScaleSelect: (StatsDateScale) -> Unit,
onDateOffsetChange: (Int) -> Unit,
onStatsTypeSelect: (StatsType) -> Unit,
allRead: Boolean = false,
) {
LazyColumn(
contentPadding = paddingValues,
Expand Down Expand Up @@ -131,7 +132,7 @@ fun StatsScreenContent(
}

item {
StatsGrid(state)
StatsGrid(state, allRead)
}
}
}
Expand Down Expand Up @@ -415,7 +416,7 @@ private fun HeroSection(
}

@Composable
private fun StatsGrid(state: StatsScreenState.Success) {
private fun StatsGrid(state: StatsScreenState.Success, allRead: Boolean = false) {
val iconColor = MaterialTheme.colorScheme.primary

Column(
Expand Down Expand Up @@ -456,7 +457,7 @@ private fun StatsGrid(state: StatsScreenState.Success) {
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Box(modifier = Modifier.weight(1f)) {
MetricCard(MetricData(state.overview.libraryMangaCount.toCountString(), "In library", null, Icons.Outlined.LibraryBooks, iconColor))
MetricCard(MetricData(state.overview.libraryMangaCount.toCountString(), if (allRead) "All read" else "In library", null, Icons.Outlined.LibraryBooks, iconColor))
}
Box(modifier = Modifier.weight(1f)) {
MetricCard(MetricData(state.titles.localMangaCount.toCountString(), "Local", null, Icons.Outlined.SdCard, iconColor))
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.DateColumnAdapter
import tachiyomi.data.History
import tachiyomi.data.Reading_sessions
import tachiyomi.data.Mangas
import tachiyomi.data.StringListColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter
Expand Down Expand Up @@ -132,6 +133,9 @@ class AppModule(val app: Application) : InjektModule {
genreAdapter = StringListColumnAdapter,
update_strategyAdapter = UpdateStrategyColumnAdapter,
),
reading_sessionsAdapter = Reading_sessions.Adapter(
read_atAdapter = DateColumnAdapter,
),
)
}
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.UpsertHistory
import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.history.model.ReadingSession
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
import tachiyomi.domain.manga.interactor.GetManga
Expand Down Expand Up @@ -146,6 +148,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(),
private val historyRepository: HistoryRepository = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
private val getIncognitoState: GetIncognitoState = Injekt.get(),
Expand Down Expand Up @@ -978,7 +981,10 @@ class ReaderViewModel @JvmOverloads constructor(
val endTime = Date()
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0

upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
if (sessionReadDuration > 0) {
upsertHistory.await(HistoryUpdate(chapterId, endTime, sessionReadDuration))
historyRepository.insertSession(ReadingSession(0, chapterId, endTime, sessionReadDuration))
}
chapterReadStartTime = null
}
}
Expand Down Expand Up @@ -2272,7 +2278,7 @@ class ReaderViewModel @JvmOverloads constructor(
if (blocks.isNotEmpty()) {
val chars = blocks.sumOf { block -> block.fullText.length }
if (chars > 0) {
com.canopus.chimareader.data.MangaStatsStorage.addStats(application, chars, timeSpent)
com.canopus.chimareader.data.MangaStatsStorage.addStats(application, chars, timeSpent, manga?.id ?: 0)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class StatsScreen : Screen() {

val screenModel = rememberScreenModel { StatsScreenModel() }
val state by screenModel.state.collectAsState()
val allRead by screenModel.allRead.collectAsState()

Scaffold(
topBar = { scrollBehavior ->
Expand All @@ -35,7 +36,6 @@ class StatsScreen : Screen() {
scrollBehavior = scrollBehavior,
// SY -->
actions = {
val allRead by screenModel.allRead.collectAsState()
AppBarActions(
persistentListOf(
AppBar.OverflowAction(
Expand Down Expand Up @@ -64,6 +64,7 @@ class StatsScreen : Screen() {
onDateScaleSelect = screenModel::setDateScale,
onDateOffsetChange = screenModel::setDateOffset,
onStatsTypeSelect = screenModel::setStatsType,
allRead = allRead,
)
}
}
Expand Down
82 changes: 44 additions & 38 deletions app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.update
import tachiyomi.domain.history.interactor.GetAllHistory
import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.history.model.History
import tachiyomi.domain.history.model.ReadingSession
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
Expand All @@ -50,6 +52,7 @@ class StatsScreenModel(
private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getTotalReadDuration: GetTotalReadDuration = Injekt.get(),
private val getAllHistory: GetAllHistory = Injekt.get(),
private val historyRepository: HistoryRepository = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val preferences: LibraryPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
Expand Down Expand Up @@ -88,7 +91,12 @@ class StatsScreenModel(
}

val allMangaHistory = getAllHistory.await()
val filteredMangaHistory = filterHistoryByScale(allMangaHistory, dateScale, dateOffset)
val allSessions = if (allRead) {
historyRepository.getAllSessions()
} else {
historyRepository.getLibrarySessions()
}
val filteredSessions = filterSessionsByScale(allSessions, dateScale, dateOffset)

val allNovels = if (statsType == StatsType.All || statsType == StatsType.Novels) {
BookStorage.loadAllBooks(context)
Expand All @@ -104,7 +112,7 @@ class StatsScreenModel(
val allNovelStatsList = novelStats.values.flatten()
val filteredNovelStats = filterNovelStatsByScale(allNovelStatsList, dateScale, dateOffset)

val mangaReadDuration = filteredMangaHistory.sumOf { it.readDuration }
val mangaReadDuration = filteredSessions.sumOf { it.duration }
val novelReadDurationSeconds = filteredNovelStats.sumOf { it.readingTime }
val novelReadDurationMs = (novelReadDurationSeconds * 1000).toLong()

Expand All @@ -115,7 +123,9 @@ class StatsScreenModel(
}

val allMangaStats = com.canopus.chimareader.data.MangaStatsStorage.loadAll(context)
val filteredMangaStats = filterMangaStatsByScale(allMangaStats, dateScale, dateOffset)
val libraryMangaIds = distinctLibraryManga.map { it.id }.toSet()
val libraryFilteredMangaStats = if (allRead) allMangaStats else allMangaStats.filter { it.mangaId in libraryMangaIds || it.mangaId == 0L }
val filteredMangaStats = filterMangaStatsByScale(libraryFilteredMangaStats, dateScale, dateOffset)

val mangaChars = filteredMangaStats.sumOf { it.charactersRead }
val mangaTimeMs = filteredMangaStats.sumOf { it.readingTime }
Expand All @@ -140,7 +150,7 @@ class StatsScreenModel(
} else null

val streak = calculateStreak(allMangaHistory, allNovelStatsList)
val historyPoints = calculateHistoryPoints(allMangaHistory, allNovelStatsList, dateScale, dateOffset)
val historyPoints = calculateHistoryPoints(allSessions, allNovelStatsList, dateScale, dateOffset)

// Calculate avg per day
val avgDurationPerDay = if (dateScale != StatsDateScale.Day && dateScale != StatsDateScale.AllTime) {
Expand Down Expand Up @@ -263,15 +273,14 @@ class StatsScreenModel(
}
}

private fun filterHistoryByScale(history: List<History>, scale: StatsDateScale, offset: Int): List<History> {
if (scale == StatsDateScale.AllTime) return history
private fun filterSessionsByScale(sessions: List<ReadingSession>, scale: StatsDateScale, offset: Int): List<ReadingSession> {
if (scale == StatsDateScale.AllTime) return sessions
val (start, end) = getDateRange(scale, offset)
val startMillis = start.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
val endMillis = end.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()

return history.filter {
val readAt = it.readAt?.time ?: 0
readAt in startMillis until endMillis

return sessions.filter {
it.readAt.time in startMillis until endMillis
}
}

Expand Down Expand Up @@ -402,7 +411,7 @@ class StatsScreenModel(
}

private fun calculateHistoryPoints(
mangaHistory: List<History>,
mangaSessions: List<ReadingSession>,
novelStats: List<Statistics>,
scale: StatsDateScale,
offset: Int,
Expand All @@ -417,7 +426,7 @@ class StatsScreenModel(
(0..6).map { daysIntoWeek ->
val date = weekStart.plusDays(daysIntoWeek.toLong())
val label = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
val value = aggregateForDate(date, mangaHistory, novelStats)
val value = aggregateForDate(date, mangaSessions, novelStats)
val pOffset = ChronoUnit.DAYS.between(now, date).toInt()
StatsData.HistoryPoint(label, value, pOffset)
}
Expand All @@ -437,7 +446,7 @@ class StatsScreenModel(
val wStart = firstMonday.plusWeeks(weeksIntoMonth.toLong())
val wEnd = wStart.plusDays(6)
val label = "W${wStart.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)}"
val value = aggregateForRange(wStart, wEnd, mangaHistory, novelStats)
val value = aggregateForRange(wStart, wEnd, mangaSessions, novelStats)
val pOffset = ChronoUnit.WEEKS.between(
now.with(DayOfWeek.MONDAY),
wStart
Expand All @@ -451,7 +460,7 @@ class StatsScreenModel(
(0..11).map { monthsIntoYear ->
val mDate = yearStart.plusMonths(monthsIntoYear.toLong())
val label = mDate.month.getDisplayName(TextStyle.SHORT, Locale.getDefault())
val value = aggregateForMonth(mDate, mangaHistory, novelStats)
val value = aggregateForMonth(mDate, mangaSessions, novelStats)
val pOffset = ChronoUnit.MONTHS.between(
now.withDayOfMonth(1),
mDate.withDayOfMonth(1)
Expand All @@ -465,7 +474,7 @@ class StatsScreenModel(
val label = yDate.year.toString()
val yStart = yDate.withDayOfYear(1)
val yEnd = yDate.withDayOfYear(yDate.lengthOfYear())
val value = aggregateForRange(yStart, yEnd, mangaHistory, novelStats)
val value = aggregateForRange(yStart, yEnd, mangaSessions, novelStats)
val pOffset = -yearsAgo
StatsData.HistoryPoint(label, value, pOffset)
}
Expand All @@ -475,59 +484,56 @@ class StatsScreenModel(

private fun aggregateForDate(
date: LocalDate,
mangaHistory: List<History>,
mangaSessions: List<ReadingSession>,
novelStats: List<Statistics>,
): Long {
val startMillis = date.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
val endMillis = date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
val mangaValue = mangaHistory
.filter { h -> (h.readAt?.time ?: 0) in startMillis until endMillis }
.sumOf { it.readDuration }

val mangaValue = mangaSessions
.filter { it.readAt.time in startMillis until endMillis }
.sumOf { it.duration }

val dateKey = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
val novelValue = novelStats
.filter { it.dateKey == dateKey }
.sumOf { (it.readingTime * 1000).toLong() }

return mangaValue + novelValue
}

private fun aggregateForRange(
start: LocalDate,
end: LocalDate,
mangaHistory: List<History>,
mangaSessions: List<ReadingSession>,
novelStats: List<Statistics>,
): Long {
val startMillis = start.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
val endMillis = end.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
val mangaValue = mangaHistory
.filter { h -> (h.readAt?.time ?: 0) in startMillis until endMillis }
.sumOf { it.readDuration }

val mangaValue = mangaSessions
.filter { it.readAt.time in startMillis until endMillis }
.sumOf { it.duration }

val startStr = start.toString()
val endStr = end.toString()
val novelValue = novelStats
.filter { it.dateKey in startStr..endStr }
.sumOf { (it.readingTime * 1000).toLong() }

return mangaValue + novelValue
}

private fun aggregateForMonth(
monthDate: LocalDate,
mangaHistory: List<History>,
mangaSessions: List<ReadingSession>,
novelStats: List<Statistics>,
): Long {
val yearMonth = YearMonth.from(monthDate)
val mangaValue = mangaHistory
.filter { h ->
val readAt = h.readAt?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDate()
readAt != null && YearMonth.from(readAt) == yearMonth
}
.sumOf { it.readDuration }

val mangaValue = mangaSessions
.filter { YearMonth.from(it.readAt.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()) == yearMonth }
.sumOf { it.duration }

val novelValue = novelStats
.filter { s ->
try {
Expand All @@ -538,7 +544,7 @@ class StatsScreenModel(
}
}
.sumOf { (it.readingTime * 1000).toLong() }

return mangaValue + novelValue
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ object MangaStatsStorage {
BookStorage.save(stats, context.filesDir, FileNames.mangaStats)
}

fun addStats(context: Context, characters: Int, timeMs: Long, date: LocalDate = LocalDate.now()) {
fun addStats(context: Context, characters: Int, timeMs: Long, mangaId: Long = 0, date: LocalDate = LocalDate.now()) {
if (characters <= 0 && timeMs <= 0) return

val dateKey = date.toString()
val allStats = loadAll(context).toMutableList()
val existing = allStats.find { it.dateKey == dateKey }
val existing = allStats.find { it.dateKey == dateKey && it.mangaId == mangaId }
if (existing != null) {
existing.charactersRead += characters
existing.readingTime += timeMs
} else {
allStats.add(MangaStats(dateKey, characters, timeMs))
allStats.add(MangaStats(dateKey, characters, timeMs, mangaId))
}
saveAll(context, allStats)
}
Expand All @@ -40,7 +40,7 @@ object MangaStatsStorage {
val local = loadAll(context).toMutableList()
var changed = false
incoming.forEach { remote ->
val existing = local.find { it.dateKey == remote.dateKey }
val existing = local.find { it.dateKey == remote.dateKey && it.mangaId == remote.mangaId }
if (existing != null) {
// If remote has higher values, we assume it's more complete or we should merge them?
// Actually, if these are daily stats, we should probably take the max or sum them depending on if they are from the same device.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ data class MangaStats(
val dateKey: String,
var charactersRead: Int = 0,
var readingTime: Long = 0, // In ms
var mangaId: Long = 0,
)
Loading
Loading