Skip to content
Open
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 @@ -3,23 +3,6 @@ package io.github.landwarderer.futon.backups.data
import androidx.collection.ArrayMap
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.DecodeSequenceMode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeToSequence
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.serializer
import org.json.JSONArray
import org.json.JSONObject
import io.github.landwarderer.futon.backups.data.model.BackupIndex
import io.github.landwarderer.futon.backups.data.model.BookmarkBackup
import io.github.landwarderer.futon.backups.data.model.CategoryBackup
Expand All @@ -37,8 +20,25 @@ import io.github.landwarderer.futon.core.util.progress.Progress
import io.github.landwarderer.futon.explore.data.MangaSourcesRepository
import io.github.landwarderer.futon.filter.data.PersistableFilter
import io.github.landwarderer.futon.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import io.github.landwarderer.futon.reader.data.TapGridSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.DecodeSequenceMode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeToSequence
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.serializer
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
Expand Down Expand Up @@ -151,6 +151,7 @@ class BackupRepository @Inject constructor(
input: ZipInputStream,
sections: Set<BackupSection>,
progress: FlowCollector<Progress>?,
isMerge: Boolean = false,
): CompositeResult {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, sections.size)
Expand All @@ -176,12 +177,12 @@ class BackupRepository @Inject constructor(
}

BackupSection.SETTINGS -> input.readMap().let {
settings.upsertAll(it)
settings.upsertAll(it, isMerge)
CompositeResult.success()
}

BackupSection.SETTINGS_READER_GRID -> input.readMap().let {
tapGridSettings.upsertAll(it)
tapGridSettings.upsertAll(it, isMerge)
CompositeResult.success()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import io.github.landwarderer.futon.R
import io.github.landwarderer.futon.core.nav.router
import io.github.landwarderer.futon.core.ui.AlertDialogFragment
Expand All @@ -20,6 +19,7 @@ import io.github.landwarderer.futon.core.util.ext.observe
import io.github.landwarderer.futon.core.util.ext.observeEvent
import io.github.landwarderer.futon.core.util.ext.textAndVisible
import io.github.landwarderer.futon.databinding.DialogRestoreBinding
import kotlinx.coroutines.flow.combine
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
Expand Down Expand Up @@ -47,8 +47,12 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
viewModel.isLoading,
viewModel.availableEntries,
viewModel.backupDate,
::Triple,
viewModel.isMergeEnabled,
::Quadruple,
).observe(viewLifecycleOwner, this::onLoadingChanged)
binding.checkboxMerge.setOnCheckedChangeListener { _, isChecked ->
viewModel.onMergeToggle(isChecked)
}
}

override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
Expand Down Expand Up @@ -76,12 +80,14 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
viewModel.onItemClick(item)
}

private fun onLoadingChanged(value: Triple<Boolean, List<BackupSectionModel>, Date?>) {
val (isLoading, entries, backupDate) = value
private fun onLoadingChanged(value: Quadruple<Boolean, List<BackupSectionModel>, Date?, Boolean>) {
val (isLoading, entries, backupDate, isMergeEnabled) = value
val hasEntries = entries.isNotEmpty()
with(requireViewBinding()) {
progressBar.isVisible = isLoading
recyclerView.isGone = isLoading
checkboxMerge.isVisible = !isLoading && hasEntries
checkboxMerge.isChecked = isMergeEnabled
textViewSubtitle.textAndVisible =
when {
!isLoading -> backupDate?.formatBackupDate()
Expand All @@ -97,6 +103,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
context ?: return false,
viewModel.uri ?: return false,
viewModel.getCheckedSections(),
viewModel.isMergeEnabled.value,
)
}

Expand All @@ -115,4 +122,11 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
.show()
dismiss()
}

data class Quadruple<out A, out B, out C, out D>(
val first: A,
val second: B,
val third: C,
val fourth: D,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class RestoreService : BaseBackupRestoreService() {
val source = intent.getStringExtra(AppRouter.KEY_DATA)?.toUriOrNull() ?: throw FileNotFoundException()
val sections =
requireNotNull(intent.getSerializableExtraCompat<Array<BackupSection>>(AppRouter.KEY_ENTRIES)?.toSet())
val isMerge = intent.getBooleanExtra(KEY_MERGE, false)
powerManager.withPartialWakeLock(TAG) {
val progress = MutableStateFlow(Progress.INDETERMINATE)
val progressUpdateJob = if (checkNotificationPermission(CHANNEL_ID)) {
Expand All @@ -62,7 +63,7 @@ class RestoreService : BaseBackupRestoreService() {
null
}
val result = ZipInputStream(contentResolver.openInputStream(source)).use { input ->
repository.restoreBackup(input, sections, progress)
repository.restoreBackup(input, sections, progress, isMerge)
}
progressUpdateJob?.cancelAndJoin()
showResultNotification(source, result)
Expand Down Expand Up @@ -102,12 +103,14 @@ class RestoreService : BaseBackupRestoreService() {

private const val TAG = "RESTORE"
private const val FOREGROUND_NOTIFICATION_ID = 39
private const val KEY_MERGE = "merge"

@CheckResult
fun start(context: Context, uri: Uri, sections: Set<BackupSection>): Boolean = try {
fun start(context: Context, uri: Uri, sections: Set<BackupSection>, isMerge: Boolean): Boolean = try {
val intent = Intent(context, RestoreService::class.java)
intent.putExtra(AppRouter.KEY_DATA, uri.toString())
intent.putExtra(AppRouter.KEY_ENTRIES, sections.toTypedArray())
intent.putExtra(KEY_MERGE, isMerge)
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class RestoreViewModel @Inject constructor(

val availableEntries = MutableStateFlow<List<BackupSectionModel>>(emptyList())
val backupDate = MutableStateFlow<Date?>(null)
val isMergeEnabled = MutableStateFlow(false)

init {
launchLoadingJob(Dispatchers.IO) {
Expand Down Expand Up @@ -80,6 +81,10 @@ class RestoreViewModel @Inject constructor(
availableEntries.value = map.values.sortedBy { it.section.ordinal }
}

fun onMergeToggle(isChecked: Boolean) {
isMergeEnabled.value = isChecked
}

fun getCheckedSections(): Set<BackupSection> = availableEntries.value
.mapNotNullTo(EnumSet.noneOf(BackupSection::class.java)) {
if (it.isChecked) it.section else null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {

fun getAllValues(): Map<String, *> = prefs.all

fun upsertAll(m: Map<String, *>) = prefs.edit {
clear()
fun upsertAll(m: Map<String, *>, isMerge: Boolean = false) = prefs.edit {
if (!isMerge) {
clear()
}
putAll(m)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import io.github.landwarderer.futon.core.util.ext.getEnumValue
import io.github.landwarderer.futon.core.util.ext.observeChanges
import io.github.landwarderer.futon.core.util.ext.putAll
import io.github.landwarderer.futon.core.util.ext.putEnumValue
import io.github.landwarderer.futon.reader.domain.TapGridArea
import io.github.landwarderer.futon.reader.ui.tapgrid.TapAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

@Reusable
Expand Down Expand Up @@ -48,8 +48,10 @@ class TapGridSettings @Inject constructor(@ApplicationContext context: Context)

fun getAllValues(): Map<String, *> = prefs.all

fun upsertAll(m: Map<String, *>) = prefs.edit {
clear()
fun upsertAll(m: Map<String, *>, isMerge: Boolean = false) = prefs.edit {
if (!isMerge) {
clear()
}
putAll(m)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import androidx.room.RawQuery
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import io.github.landwarderer.futon.core.db.entity.MangaEntity
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import io.github.landwarderer.futon.core.db.entity.MangaEntity
import kotlin.collections.forEach

@Dao
abstract class StatsDao {
Expand Down Expand Up @@ -66,6 +65,40 @@ abstract class StatsDao {
query: SupportSQLiteQuery
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>

suspend fun getTagDurationStats(
fromDate: Long,
isNsfw: Boolean?,
favouriteCategories: Set<Long>
): Map<String, Long> {
val conditions = ArrayList<String>()
conditions.add("(SELECT deleted_at FROM history WHERE history.manga_id = stats.manga_id) = 0")
conditions.add("stats.started_at >= $fromDate")
if (favouriteCategories.isNotEmpty()) {
val ids = favouriteCategories.joinToString(",")
conditions.add("stats.manga_id IN (SELECT manga_id FROM favourites WHERE category_id IN ($ids))")
}
if (isNsfw != null) {
val flag = if (isNsfw) 1 else 0
conditions.add("manga.nsfw = $flag")
}
val where = conditions.joinToString(separator = " AND ")
val query = SimpleSQLiteQuery(
"SELECT tags.title AS tag_name, SUM(stats.duration) AS d " +
"FROM stats " +
"LEFT JOIN manga ON manga.manga_id = stats.manga_id " +
"LEFT JOIN manga_tags ON manga_tags.manga_id = manga.manga_id " +
"LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id " +
"WHERE $where AND tags.title IS NOT NULL " +
"GROUP BY tags.tag_id ORDER BY d DESC",
)
return getTagDurationStatsImpl(query)
}

@RawQuery
protected abstract suspend fun getTagDurationStatsImpl(
query: SupportSQLiteQuery
): Map<@MapColumn("tag_name") String, @MapColumn("d") Long>

@Query("SELECT * FROM stats ORDER BY started_at LIMIT :limit OFFSET :offset")
protected abstract suspend fun findAll(offset: Int, limit: Int): List<StatsEntity>
fun dumpEnabled(): Flow<StatsEntity> = flow {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package io.github.landwarderer.futon.stats.data

import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import io.github.landwarderer.futon.core.db.MangaDatabase
import io.github.landwarderer.futon.core.db.entity.toManga
import io.github.landwarderer.futon.core.prefs.AppSettings
import io.github.landwarderer.futon.core.prefs.observeAsFlow
import io.github.landwarderer.futon.stats.domain.StatsPeriod
import io.github.landwarderer.futon.stats.domain.StatsRecord
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import java.util.NavigableMap
import java.util.TreeMap
import java.util.concurrent.TimeUnit
Expand All @@ -22,15 +22,41 @@ class StatsRepository @Inject constructor(
private val db: MangaDatabase,
) {

suspend fun getReadingStats(period: StatsPeriod, categories: Set<Long>): List<StatsRecord> {
suspend fun getReadingStats(
period: StatsPeriod,
categories: Set<Long>,
byGenre: Boolean = false,
): List<StatsRecord> {
val fromDate = if (period == StatsPeriod.ALL) {
0L
} else {
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
}
if (byGenre) {
val stats = db.getStatsDao().getTagDurationStats(fromDate, null, categories)
val total = stats.values.sum()
val result = ArrayList<StatsRecord>(stats.size)
var other = StatsRecord(manga = null, tagName = null, duration = 0)
for ((tagName, duration) in stats) {
val percent = duration.toDouble() / total
if (percent < 0.05) {
other = other.copy(duration = other.duration + duration)
} else {
result += StatsRecord(
manga = null,
tagName = tagName,
duration = duration,
)
}
}
if (other.duration != 0L) {
result += other
}
return result
}
val stats = db.getStatsDao().getDurationStats(fromDate, null, categories)
val result = ArrayList<StatsRecord>(stats.size)
var other = StatsRecord(null, 0)
var other = StatsRecord(manga = null, tagName = null, duration = 0)
val total = stats.values.sum()
for ((mangaEntity, duration) in stats) {
val manga = mangaEntity.toManga(emptySet(), null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import java.util.concurrent.TimeUnit

data class StatsRecord(
val manga: Manga?,
val tagName: String? = null,
val duration: Long,
) : ListModel {

override fun areItemsTheSame(other: ListModel): Boolean {
return other is StatsRecord && other.manga == manga
return other is StatsRecord && other.manga == manga && other.tagName == tagName
}

val time: ReadingTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import io.github.landwarderer.futon.R
import io.github.landwarderer.futon.core.ui.list.OnListItemClickListener
import io.github.landwarderer.futon.core.util.FutonColors
import io.github.landwarderer.futon.databinding.ItemStatsBinding
import org.koitharu.kotatsu.parsers.model.Manga
import io.github.landwarderer.futon.stats.domain.StatsRecord
import org.koitharu.kotatsu.parsers.model.Manga

fun statsAD(
listener: OnListItemClickListener<Manga>,
Expand All @@ -16,11 +16,11 @@ fun statsAD(
) {

binding.root.setOnClickListener { v ->
listener.onItemClick(item.manga ?: return@setOnClickListener, v)
item.manga?.let { listener.onItemClick(it, v) }
}

bind {
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
binding.textViewTitle.text = item.manga?.title ?: item.tagName ?: getString(R.string.other_manga)
binding.textViewSummary.text = item.time.format(context.resources)
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(FutonColors.ofManga(context, item.manga))
binding.root.isClickable = item.manga != null
Expand Down
Loading
Loading