Misc cleanup

- Rebased against master
- Removed unnecessary tabs for now
This commit is contained in:
arkon 2023-11-19 11:50:41 -05:00
parent 255ed50685
commit 14eb114ee8
18 changed files with 478 additions and 2 deletions

View File

@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 109
versionCode = 110
versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@ -29,6 +29,7 @@ import tachiyomi.data.manga.MangaRepositoryImpl
import tachiyomi.data.release.ReleaseServiceImpl
import tachiyomi.data.source.SourceRepositoryImpl
import tachiyomi.data.source.StubSourceRepositoryImpl
import tachiyomi.data.stat.DownloadStatRepositoryImpl
import tachiyomi.data.track.TrackRepositoryImpl
import tachiyomi.data.updates.UpdatesRepositoryImpl
import tachiyomi.domain.category.interactor.CreateCategoryWithName
@ -72,6 +73,9 @@ import tachiyomi.domain.source.interactor.GetRemoteManga
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.repository.SourceRepository
import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.stat.interactor.AddDownloadStatOperation
import tachiyomi.domain.stat.interactor.GetDownloadStatOperations
import tachiyomi.domain.stat.repository.DownloadStatRepository
import tachiyomi.domain.track.interactor.DeleteTrack
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.GetTracksPerManga
@ -167,5 +171,9 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) }
addSingletonFactory<DownloadStatRepository> { DownloadStatRepositoryImpl(get()) }
addFactory { GetDownloadStatOperations(get()) }
addFactory { AddDownloadStatOperation(get()) }
}
}

View File

@ -0,0 +1,75 @@
package eu.kanade.presentation.more.download
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.download.components.DeletedStatsRow
import eu.kanade.presentation.more.download.components.DownloadStatOverviewSection
import eu.kanade.presentation.more.download.components.DownloadStatsRow
import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
class DownloadStatsScreen : Screen() {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { DownloadStatsScreenModel() }
val state by screenModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(MR.strings.label_download_stats),
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
) { contentPadding ->
when {
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
else -> {
OverallStats(
state = state,
contentPadding = contentPadding,
)
}
}
}
}
}
@Composable
private fun OverallStats(
state: DownloadStatsScreenModel.State,
contentPadding: PaddingValues,
) {
LazyColumn(
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
item {
DownloadStatOverviewSection(state.uniqueItems)
}
item {
DownloadStatsRow(state.downloadStatOperations.filter { it.size > 0 })
}
item {
DeletedStatsRow(state.downloadStatOperations.filter { it.size < 0 })
}
}
}

View File

@ -0,0 +1,101 @@
package eu.kanade.presentation.more.download
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.presentation.more.download.data.DownloadStatManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.stat.interactor.GetDownloadStatOperations
import tachiyomi.domain.stat.model.DownloadStatOperation
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class DownloadStatsScreenModel(
private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadProvider: DownloadProvider = Injekt.get(),
private val getDownloadStatOperations: GetDownloadStatOperations = Injekt.get(),
) : StateScreenModel<DownloadStatsScreenModel.State>(State()) {
init {
screenModelScope.launchIO {
mutableState.update { state ->
state.copy(
items = getLibraryManga.await().map { libraryManga ->
val source = sourceManager.getOrStub(libraryManga.manga.source)
val path = downloadProvider.findMangaDir(
libraryManga.manga.title,
source,
)?.filePath
DownloadStatManga(
libraryManga = libraryManga,
folderSize = if (path != null) DiskUtil.getDirectorySize(File(path)) else 0,
downloadChaptersCount = downloadManager.getDownloadCount(libraryManga.manga),
)
},
downloadStatOperations = getDownloadStatOperations.await(),
isLoading = false,
)
}
}
screenModelScope.launchIO {
getDownloadStatOperations.subscribe()
.distinctUntilChanged()
.collectLatest { operations ->
mutableState.update { state ->
val oldOperationsId = state.downloadStatOperations.map { it.id }.toHashSet()
val newOperations = operations
.mapNotNull { if (!oldOperationsId.contains(it.id)) it else null }
.groupBy { it.mangaId }
val newItems = state.items.map { item ->
if (newOperations.containsKey(item.libraryManga.id)) {
item.copy(
folderSize = item.folderSize +
newOperations[item.libraryManga.id]!!
.sumOf { it.size },
downloadChaptersCount = item.downloadChaptersCount +
newOperations[item.libraryManga.id]!!.sumOf { it.units }.toInt(),
)
} else {
item
}
}
state.copy(
items = newItems,
downloadStatOperations = operations,
)
}
}
}
}
@Immutable
data class State(
val isLoading: Boolean = true,
val items: List<DownloadStatManga> = emptyList(),
val downloadStatOperations: List<DownloadStatOperation> = emptyList(),
) {
val uniqueItems: List<DownloadStatManga> by lazy {
val uniqueIds = HashSet<Long>()
val uniqueMangas = mutableListOf<DownloadStatManga>()
for (manga in items) {
if (uniqueIds.add(manga.libraryManga.manga.id)) {
uniqueMangas.add(manga)
}
}
uniqueMangas
}
}
}

View File

@ -0,0 +1,85 @@
package eu.kanade.presentation.more.download.components
import android.text.format.Formatter
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.Storage
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.more.download.data.DownloadStatManga
import eu.kanade.presentation.more.stats.components.StatsItem
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
import eu.kanade.presentation.more.stats.components.StatsSection
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.math.abs
@Composable
fun DownloadStatOverviewSection(
items: List<DownloadStatManga>,
) {
StatsSection(MR.strings.label_overview_section) {
Row {
StatsOverviewItem(
title = folderSizeText(items.sumOf { it.folderSize }),
subtitle = stringResource(MR.strings.label_size),
icon = Icons.Outlined.Storage,
)
StatsOverviewItem(
title = items.sumOf { it.downloadChaptersCount }.toString(),
subtitle = stringResource(MR.strings.chapters),
icon = Icons.Outlined.Book,
)
StatsOverviewItem(
title = items.filter { it.downloadChaptersCount > 0 }.size.toString(),
subtitle = stringResource(MR.strings.manga),
icon = Icons.Outlined.CollectionsBookmark,
)
}
}
}
@Composable
fun DownloadStatsRow(
data: List<DownloadStatOperation>,
) {
StatsSection(MR.strings.downloaded_chapters) {
Row {
StatsItem(
data.size.toString(),
stringResource(MR.strings.label_total_chapters),
)
StatsItem(
folderSizeText(data.sumOf { abs(it.size) }),
stringResource(MR.strings.label_size),
)
}
}
}
@Composable
fun DeletedStatsRow(
data: List<DownloadStatOperation>,
) {
StatsSection(MR.strings.deleted_chapters) {
Row {
StatsItem(
abs(data.sumOf { it.units }).toString(),
stringResource(MR.strings.label_total_chapters),
)
StatsItem(
folderSizeText(abs(data.sumOf { it.size })),
stringResource(MR.strings.label_size),
)
}
}
}
@Composable
private fun folderSizeText(folderSize: Long): String {
val context = LocalContext.current
return Formatter.formatFileSize(context, folderSize)
}

View File

@ -0,0 +1,11 @@
package eu.kanade.presentation.more.download.data
import androidx.compose.runtime.Immutable
import tachiyomi.domain.library.model.LibraryManga
@Immutable
data class DownloadStatManga(
val libraryManga: LibraryManga,
val folderSize: Long = 0,
val downloadChaptersCount: Int = 0,
)

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.download.DownloadStatsScreen
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
@ -255,6 +256,7 @@ object SettingsDataScreen : SearchableSettings {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
val navigator = LocalNavigator.currentOrThrow
val chapterCache = remember { Injekt.get<ChapterCache>() }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
@ -287,6 +289,10 @@ object SettingsDataScreen : SearchableSettings {
pref = libraryPreferences.autoClearChapterCache(),
title = stringResource(MR.strings.pref_auto_clear_chapter_cache),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.label_download_stats),
onClick = { navigator.push(DownloadStatsScreen()) },
),
),
)
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.drop
@ -22,9 +23,12 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.stat.interactor.AddDownloadStatOperation
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
/**
* This class is used to manage chapter downloads in the application. It must be instantiated once
@ -38,6 +42,7 @@ class DownloadManager(
private val getCategories: GetCategories = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val addDownloadStatOperation: AddDownloadStatOperation = Injekt.get(),
) {
/**
@ -224,6 +229,13 @@ class DownloadManager(
removeFromDownloadQueue(filteredChapters)
val (mangaDir, chapterDirs) = provider.findChapterDirs(filteredChapters, manga, source)
addDownloadStatOperation.await(
DownloadStatOperation.create().copy(
mangaId = manga.id,
size = chapterDirs.sumOf { DiskUtil.getDirectorySize(File(it.filePath!!)) } * -1,
units = filteredChapters.size.toLong() * -1,
),
)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
@ -246,7 +258,18 @@ class DownloadManager(
if (removeQueued) {
downloader.removeFromQueue(manga)
}
provider.findMangaDir(manga.title, source)?.delete()
val mangaDir = provider.findMangaDir(manga.title, source)
val dirSize = DiskUtil.getDirectorySize(File(mangaDir?.filePath!!))
if (dirSize > 0) {
addDownloadStatOperation.await(
DownloadStatOperation.create().copy(
mangaId = manga.id,
size = dirSize * -1,
units = cache.getDownloadCount(manga).toLong() * -1,
),
)
}
mangaDir.delete()
cache.removeManga(manga)
// Delete source directory if empty

View File

@ -54,6 +54,8 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.stat.interactor.AddDownloadStatOperation
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -75,6 +77,7 @@ class Downloader(
private val sourceManager: SourceManager = Injekt.get(),
private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val addDownloadStatOperation: AddDownloadStatOperation = Injekt.get(),
private val xml: XML = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) {
@ -406,6 +409,22 @@ class Downloader(
DiskUtil.createNoMediaFile(tmpDir, context)
addDownloadStatOperation.await(
DownloadStatOperation.create().copy(
mangaId = download.manga.id,
size = DiskUtil.getDirectorySize(
File(
provider.findChapterDir(
download.chapter.name,
download.chapter.scanlator,
download.manga.title,
download.source,
)?.filePath!!,
),
),
),
)
download.status = Download.State.DOWNLOADED
} catch (error: Throwable) {
if (error is CancellationException) throw error

View File

@ -0,0 +1,19 @@
package tachiyomi.data.stat
import tachiyomi.domain.stat.model.DownloadStatOperation
object DownloadStatMapper {
fun map(
id: Long,
mangaId: Long?,
date: Long,
size: Long,
units: Long,
): DownloadStatOperation = DownloadStatOperation(
id = id,
mangaId = mangaId,
date = date * 1000,
size = size,
units = units,
)
}

View File

@ -0,0 +1,30 @@
package tachiyomi.data.stat
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.domain.stat.repository.DownloadStatRepository
class DownloadStatRepositoryImpl(
private val handler: DatabaseHandler,
) : DownloadStatRepository {
override suspend fun getStatOperations(): List<DownloadStatOperation> {
return handler.awaitList { download_statQueries.getStatOperations(DownloadStatMapper::map) }
}
override suspend fun getStatOperationsAsFlow(): Flow<List<DownloadStatOperation>> {
return handler.subscribeToList { download_statQueries.getStatOperations(DownloadStatMapper::map) }
}
override suspend fun insert(operation: DownloadStatOperation) {
handler.await {
download_statQueries.insert(
mangaId = operation.mangaId,
date = operation.date / 1000,
size = operation.size,
units = operation.units,
)
}
}
}

View File

@ -0,0 +1,18 @@
CREATE TABLE download_stat(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER,
date INTEGER NOT NULL DEFAULT 0,
size INTEGER NOT NULL,
units INTEGER NOT NULL,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE SET NULL
);
-- Methods
getStatOperations:
SELECT * FROM download_stat;
insert:
INSERT INTO download_stat(manga_id, date, size, units)
VALUES (:mangaId, :date, :size, :units);

View File

@ -0,0 +1,10 @@
CREATE TABLE download_stat(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER,
date INTEGER NOT NULL DEFAULT 0,
size INTEGER NOT NULL,
units INTEGER NOT NULL,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE SET NULL
);

View File

@ -0,0 +1,13 @@
package tachiyomi.domain.stat.interactor
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.domain.stat.repository.DownloadStatRepository
class AddDownloadStatOperation(
private val repository: DownloadStatRepository,
) {
suspend fun await(actions: DownloadStatOperation) {
repository.insert(actions)
}
}

View File

@ -0,0 +1,18 @@
package tachiyomi.domain.stat.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.stat.model.DownloadStatOperation
import tachiyomi.domain.stat.repository.DownloadStatRepository
class GetDownloadStatOperations(
private val repository: DownloadStatRepository,
) {
suspend fun await(): List<DownloadStatOperation> {
return repository.getStatOperations()
}
suspend fun subscribe(): Flow<List<DownloadStatOperation>> {
return repository.getStatOperationsAsFlow()
}
}

View File

@ -0,0 +1,21 @@
package tachiyomi.domain.stat.model
import java.util.Date
data class DownloadStatOperation(
val id: Long,
val mangaId: Long?,
val date: Long,
val size: Long,
val units: Long,
) {
companion object {
fun create() = DownloadStatOperation(
id = -1,
mangaId = -1,
date = Date().time,
size = -1,
units = 1,
)
}
}

View File

@ -0,0 +1,13 @@
package tachiyomi.domain.stat.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.stat.model.DownloadStatOperation
interface DownloadStatRepository {
suspend fun getStatOperations(): List<DownloadStatOperation>
suspend fun getStatOperationsAsFlow(): Flow<List<DownloadStatOperation>>
suspend fun insert(operation: DownloadStatOperation)
}

View File

@ -31,6 +31,7 @@
<string name="label_backup">Backup and restore</string>
<string name="label_data_storage">Data and storage</string>
<string name="label_stats">Statistics</string>
<string name="label_download_stats">Download statistics</string>
<string name="label_migration">Migrate</string>
<string name="label_extensions">Extensions</string>
<string name="label_extension_info">Extension info</string>
@ -166,6 +167,11 @@
<string name="action_faq_and_guides">FAQ and Guides</string>
<string name="action_not_now">Not now</string>
<!-- downloads stats screen -->
<string name="label_download_stats_overall_tab">Overall</string>
<string name="label_size">Size</string>
<string name="deleted_chapters">Deleted chapters</string>
<!-- Operations -->
<string name="loading">Loading…</string>
<string name="internal_error">InternalError: Check crash logs for further information</string>