mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 16:09:17 +01:00
add download stats screen
This commit is contained in:
parent
2ee895ee3c
commit
c14fcfdec3
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.systemBars
|
|||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CloudOff
|
import androidx.compose.material.icons.outlined.CloudOff
|
||||||
|
import androidx.compose.material.icons.outlined.FileDownloadDone
|
||||||
import androidx.compose.material.icons.outlined.GetApp
|
import androidx.compose.material.icons.outlined.GetApp
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
@ -48,6 +49,7 @@ fun MoreScreen(
|
|||||||
onClickDataAndStorage: () -> Unit,
|
onClickDataAndStorage: () -> Unit,
|
||||||
onClickSettings: () -> Unit,
|
onClickSettings: () -> Unit,
|
||||||
onClickAbout: () -> Unit,
|
onClickAbout: () -> Unit,
|
||||||
|
onClickDownloadState: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
@ -139,6 +141,13 @@ fun MoreScreen(
|
|||||||
onPreferenceClick = onClickStats,
|
onPreferenceClick = onClickStats,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(R.string.label_download_stats),
|
||||||
|
icon = Icons.Outlined.FileDownloadDone,
|
||||||
|
onPreferenceClick = onClickDownloadState,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.label_data_storage),
|
title = stringResource(R.string.label_data_storage),
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.presentation.more.download
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DownloadStatManga(
|
||||||
|
val libraryManga: LibraryManga,
|
||||||
|
val folderSize: Long,
|
||||||
|
val selected: Boolean = false,
|
||||||
|
val source: Source,
|
||||||
|
val category: Category,
|
||||||
|
val downloadChaptersCount: Int,
|
||||||
|
)
|
@ -0,0 +1,309 @@
|
|||||||
|
package eu.kanade.presentation.more.download
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.DensitySmall
|
||||||
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
|
import androidx.compose.material.icons.outlined.Sort
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
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.components.AppBarActions
|
||||||
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.SortItem
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
class DownloadStatsScreen : Screen() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { DownloadStatsScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
DownloadStatsAppBar(
|
||||||
|
groupByMode = state.groupByMode,
|
||||||
|
selected = state.selected,
|
||||||
|
onSelectAll = { screenModel.toggleAllSelection(true) },
|
||||||
|
onInvertSelection = { screenModel.invertSelection() },
|
||||||
|
onCancelActionMode = { screenModel.toggleAllSelection(false) },
|
||||||
|
onMultiDeleteClicked = screenModel::deleteMangas,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onClickGroup = screenModel::runGroupBy,
|
||||||
|
onClickSort = screenModel::runSortAction,
|
||||||
|
sortState = state.sortMode,
|
||||||
|
descendingOrder = state.descendingOrder,
|
||||||
|
searchQuery = state.searchQuery,
|
||||||
|
onChangeSearchQuery = screenModel::search,
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
when {
|
||||||
|
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
|
|
||||||
|
state.processedItems.isEmpty() -> EmptyScreen(
|
||||||
|
textResource = R.string.information_no_downloads,
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
when (state.groupByMode) {
|
||||||
|
GroupByMode.NONE -> FastScrollLazyColumn(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
) {
|
||||||
|
downloadStatUiItems(
|
||||||
|
items = state.processedItems,
|
||||||
|
selectionMode = state.selectionMode,
|
||||||
|
onClick = { item ->
|
||||||
|
navigator.push(
|
||||||
|
MangaScreen(item.libraryManga.manga.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSelected = screenModel::toggleSelection,
|
||||||
|
onDeleteManga = screenModel::deleteMangas,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupByMode.BY_SOURCE -> {
|
||||||
|
CategoryList(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
selectionMode = state.selectionMode,
|
||||||
|
onMangaClick = { item ->
|
||||||
|
navigator.push(
|
||||||
|
MangaScreen(item.libraryManga.manga.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDeleteManga = screenModel::deleteMangas,
|
||||||
|
onGroupSelected = screenModel::groupSelection,
|
||||||
|
onSelected = screenModel::toggleSelection,
|
||||||
|
categoryMap = screenModel.categoryMap(state.processedItems, GroupByMode.BY_SOURCE, state.sortMode, state.descendingOrder),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupByMode.BY_CATEGORY -> {
|
||||||
|
CategoryList(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
selectionMode = state.selectionMode,
|
||||||
|
onMangaClick = { item ->
|
||||||
|
navigator.push(
|
||||||
|
MangaScreen(item.libraryManga.manga.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDeleteManga = screenModel::deleteMangas,
|
||||||
|
onGroupSelected = screenModel::groupSelection,
|
||||||
|
onSelected = screenModel::toggleSelection,
|
||||||
|
categoryMap = screenModel.categoryMap(state.processedItems, GroupByMode.BY_CATEGORY, state.sortMode, state.descendingOrder),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadStatsAppBar(
|
||||||
|
groupByMode: GroupByMode,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: List<DownloadStatManga>,
|
||||||
|
onSelectAll: () -> Unit,
|
||||||
|
onInvertSelection: () -> Unit,
|
||||||
|
onCancelActionMode: () -> Unit,
|
||||||
|
onClickSort: (SortingMode) -> Unit,
|
||||||
|
onClickGroup: (GroupByMode) -> Unit,
|
||||||
|
onMultiDeleteClicked: (List<DownloadStatManga>) -> Unit,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
sortState: SortingMode,
|
||||||
|
descendingOrder: Boolean? = null,
|
||||||
|
searchQuery: String?,
|
||||||
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
|
navigateUp: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
DownloadStatsActionAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
selected = selected,
|
||||||
|
onSelectAll = onSelectAll,
|
||||||
|
onInvertSelection = onInvertSelection,
|
||||||
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SearchToolbar(
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
titleContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.label_download_stats),
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.weight(1f, false),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
|
actions = {
|
||||||
|
val filterTint = LocalContentColor.current
|
||||||
|
var groupExpanded by remember { mutableStateOf(false) }
|
||||||
|
val onDownloadDismissRequest = { groupExpanded = false }
|
||||||
|
GroupDropdownMenu(
|
||||||
|
expanded = groupExpanded,
|
||||||
|
groupByMode = groupByMode,
|
||||||
|
onDismissRequest = onDownloadDismissRequest,
|
||||||
|
onGroupClicked = onClickGroup,
|
||||||
|
)
|
||||||
|
var sortExpanded by remember { mutableStateOf(false) }
|
||||||
|
val onSortDismissRequest = { sortExpanded = false }
|
||||||
|
SortDropdownMenu(
|
||||||
|
expanded = sortExpanded,
|
||||||
|
onDismissRequest = onSortDismissRequest,
|
||||||
|
onSortClicked = onClickSort,
|
||||||
|
sortState = sortState,
|
||||||
|
descendingOrder = descendingOrder,
|
||||||
|
)
|
||||||
|
AppBarActions(
|
||||||
|
listOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_sort),
|
||||||
|
icon = Icons.Outlined.Sort,
|
||||||
|
iconTint = filterTint,
|
||||||
|
onClick = { sortExpanded = !sortExpanded },
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_group),
|
||||||
|
icon = Icons.Outlined.DensitySmall,
|
||||||
|
onClick = { groupExpanded = !groupExpanded },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadStatsActionAppBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: List<DownloadStatManga>,
|
||||||
|
onSelectAll: () -> Unit,
|
||||||
|
onInvertSelection: () -> Unit,
|
||||||
|
onCancelActionMode: () -> Unit,
|
||||||
|
onMultiDeleteClicked: (List<DownloadStatManga>) -> Unit,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigateUp: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
title = stringResource(R.string.label_download_stats),
|
||||||
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
listOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_select_all),
|
||||||
|
icon = Icons.Outlined.SelectAll,
|
||||||
|
onClick = onSelectAll,
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_select_inverse),
|
||||||
|
icon = Icons.Outlined.FlipToBack,
|
||||||
|
onClick = onInvertSelection,
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.delete_downloads_for_manga),
|
||||||
|
icon = Icons.Outlined.Delete,
|
||||||
|
onClick = { onMultiDeleteClicked(selected) },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
groupByMode: GroupByMode,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onGroupClicked: (GroupByMode) -> Unit,
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
listOfNotNull(
|
||||||
|
if (groupByMode != GroupByMode.NONE) GroupByMode.NONE to stringResource(R.string.action_ungroup) else null,
|
||||||
|
if (groupByMode != GroupByMode.BY_CATEGORY) GroupByMode.BY_CATEGORY to stringResource(R.string.action_group_by_category) else null,
|
||||||
|
if (groupByMode != GroupByMode.BY_SOURCE) GroupByMode.BY_SOURCE to stringResource(R.string.action_group_by_source) else null,
|
||||||
|
).map { (mode, string) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = string) },
|
||||||
|
onClick = {
|
||||||
|
onGroupClicked(mode)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSortClicked: (SortingMode) -> Unit,
|
||||||
|
sortState: SortingMode,
|
||||||
|
descendingOrder: Boolean? = null,
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
listOfNotNull(
|
||||||
|
SortingMode.BY_ALPHABET to stringResource(R.string.action_sort_alpha),
|
||||||
|
SortingMode.BY_SIZE to stringResource(R.string.action_sort_size),
|
||||||
|
).map { (mode, string) ->
|
||||||
|
SortItem(
|
||||||
|
label = string,
|
||||||
|
sortDescending = descendingOrder.takeIf { sortState == mode },
|
||||||
|
onClick = { onSortClicked(mode) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,355 @@
|
|||||||
|
package eu.kanade.presentation.more.download
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.core.util.addOrRemove
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.preference.getEnum
|
||||||
|
import tachiyomi.core.util.lang.launchIO
|
||||||
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.TreeMap
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
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 getCategories: GetCategories = Injekt.get(),
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
|
) : StateScreenModel<DownloadStatsScreenState>(DownloadStatsScreenState()) {
|
||||||
|
|
||||||
|
private val downloadCache: DownloadCache by injectLazy()
|
||||||
|
|
||||||
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
private val selectedMangaIds: HashSet<Long> = HashSet()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
val sortMode = preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).get()
|
||||||
|
mutableState.update {
|
||||||
|
val categories = getCategories.await().associateBy { group -> group.id }
|
||||||
|
it.copy(
|
||||||
|
items = getLibraryManga.await().filter { libraryManga ->
|
||||||
|
downloadCache.getDownloadCount(libraryManga.manga) > 0
|
||||||
|
}.map { libraryManga ->
|
||||||
|
val source = sourceManager.get(libraryManga.manga.source)!!
|
||||||
|
DownloadStatManga(
|
||||||
|
libraryManga = libraryManga,
|
||||||
|
selected = libraryManga.id in selectedMangaIds,
|
||||||
|
source = source,
|
||||||
|
folderSize = getFolderSize(
|
||||||
|
downloadProvider.findMangaDir(
|
||||||
|
libraryManga.manga.title,
|
||||||
|
source,
|
||||||
|
)?.filePath!!,
|
||||||
|
),
|
||||||
|
downloadChaptersCount = downloadCache.getDownloadCount(libraryManga.manga),
|
||||||
|
category = categories[libraryManga.category]!!,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
groupByMode = preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).get(),
|
||||||
|
sortMode = sortMode,
|
||||||
|
descendingOrder = preferenceStore.getBoolean("descending_order", false).get(),
|
||||||
|
searchQuery = preferenceStore.getString("search_query", "").get().takeIf { string -> string != "" },
|
||||||
|
isLoading = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
runSortAction(sortMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFolderSize(path: String): Long {
|
||||||
|
val file = File(path)
|
||||||
|
var size: Long = 0
|
||||||
|
|
||||||
|
if (file.exists()) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
val files = file.listFiles()
|
||||||
|
if (files != null) {
|
||||||
|
for (childFile in files) {
|
||||||
|
size += if (childFile.isDirectory) {
|
||||||
|
getFolderSize(childFile.path)
|
||||||
|
} else {
|
||||||
|
getFileSize(childFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size = getFileSize(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileSize(file: File): Long {
|
||||||
|
val archiveFormats = setOf(".zip", ".cbz", ".rar", ".cbr")
|
||||||
|
return if (file.isDirectory) {
|
||||||
|
getFolderSize(file.path)
|
||||||
|
} else if (file.isFile) {
|
||||||
|
file.length()
|
||||||
|
} else if (file.extension.lowercase() in archiveFormats) {
|
||||||
|
getZipFileSize(file)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getZipFileSize(file: File): Long {
|
||||||
|
var size: Long = 0
|
||||||
|
|
||||||
|
val zipFile = ZipFile(file)
|
||||||
|
val entries = zipFile.entries()
|
||||||
|
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
val entry: ZipEntry = entries.nextElement()
|
||||||
|
size += entry.size
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile.close()
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runSortAction(mode: SortingMode) {
|
||||||
|
when (mode) {
|
||||||
|
SortingMode.BY_ALPHABET -> sortByAlphabet()
|
||||||
|
SortingMode.BY_SIZE -> sortBySize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runGroupBy(mode: GroupByMode) {
|
||||||
|
when (mode) {
|
||||||
|
GroupByMode.NONE -> unGroup()
|
||||||
|
GroupByMode.BY_CATEGORY -> groupByCategory()
|
||||||
|
GroupByMode.BY_SOURCE -> groupBySource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortByAlphabet() {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val descendingOrder = if (state.sortMode == SortingMode.BY_ALPHABET) !state.descendingOrder else false
|
||||||
|
preferenceStore.getBoolean("descending_order", false).set(descendingOrder)
|
||||||
|
state.copy(
|
||||||
|
items = if (descendingOrder) state.items.sortedByDescending { it.libraryManga.manga.title } else state.items.sortedBy { it.libraryManga.manga.title },
|
||||||
|
descendingOrder = descendingOrder,
|
||||||
|
sortMode = SortingMode.BY_ALPHABET,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).set(SortingMode.BY_ALPHABET)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun categoryMap(items: List<DownloadStatManga>, groupMode: GroupByMode, sortMode: SortingMode, descendingOrder: Boolean): Map<String, List<DownloadStatManga>> {
|
||||||
|
val unsortedMap = when (groupMode) {
|
||||||
|
GroupByMode.BY_CATEGORY -> items.groupBy { if (it.category.isSystemCategory) { stringResource(R.string.label_default) } else { it.category.name } }
|
||||||
|
GroupByMode.BY_SOURCE -> items.groupBy { it.source.name }
|
||||||
|
GroupByMode.NONE -> emptyMap()
|
||||||
|
}
|
||||||
|
return when (sortMode) {
|
||||||
|
SortingMode.BY_ALPHABET -> {
|
||||||
|
val sortedMap = TreeMap<String, List<DownloadStatManga>>(if (descendingOrder) { compareByDescending { it } } else { compareBy { it } })
|
||||||
|
sortedMap.putAll(unsortedMap)
|
||||||
|
sortedMap
|
||||||
|
}
|
||||||
|
SortingMode.BY_SIZE -> {
|
||||||
|
val compareFun: (String) -> Comparable<*> = { it: String -> unsortedMap[it]?.sumOf { manga -> manga.folderSize } ?: 0 }
|
||||||
|
val sortedMap = TreeMap<String, List<DownloadStatManga>>(if (descendingOrder) { compareByDescending { compareFun(it) } } else { compareBy { compareFun(it) } })
|
||||||
|
sortedMap.putAll(unsortedMap)
|
||||||
|
sortedMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortBySize() {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val descendingOrder = if (state.sortMode == SortingMode.BY_SIZE) !state.descendingOrder else false
|
||||||
|
preferenceStore.getBoolean("descending_order", false).set(descendingOrder)
|
||||||
|
state.copy(
|
||||||
|
items = if (descendingOrder) state.items.sortedByDescending { it.folderSize } else state.items.sortedBy { it.folderSize },
|
||||||
|
descendingOrder = descendingOrder,
|
||||||
|
sortMode = SortingMode.BY_SIZE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("sort_mode", SortingMode.BY_SIZE).set(SortingMode.BY_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupBySource() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupByMode = GroupByMode.BY_SOURCE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.BY_SOURCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupByCategory() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupByMode = GroupByMode.BY_CATEGORY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.BY_CATEGORY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unGroup() {
|
||||||
|
mutableState.update {
|
||||||
|
it.copy(
|
||||||
|
groupByMode = GroupByMode.NONE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSelection(
|
||||||
|
item: DownloadStatManga,
|
||||||
|
selected: Boolean,
|
||||||
|
userSelected: Boolean = false,
|
||||||
|
fromLongPress: Boolean = false,
|
||||||
|
) {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.toMutableList().apply {
|
||||||
|
val selectedIndex = indexOfFirst { it.libraryManga.manga.id == item.libraryManga.manga.id }
|
||||||
|
if (selectedIndex < 0) return@apply
|
||||||
|
|
||||||
|
val selectedItem = get(selectedIndex)
|
||||||
|
if (selectedItem.selected == selected) return@apply
|
||||||
|
|
||||||
|
val firstSelection = none { it.selected }
|
||||||
|
set(selectedIndex, selectedItem.copy(selected = selected))
|
||||||
|
selectedMangaIds.addOrRemove(item.libraryManga.manga.id, selected)
|
||||||
|
|
||||||
|
if (selected && userSelected && fromLongPress) {
|
||||||
|
if (firstSelection) {
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
|
} else {
|
||||||
|
// Try to select the items in-between when possible
|
||||||
|
val range: IntRange
|
||||||
|
if (selectedIndex < selectedPositions[0]) {
|
||||||
|
range = selectedIndex + 1 until selectedPositions[0]
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
} else if (selectedIndex > selectedPositions[1]) {
|
||||||
|
range = (selectedPositions[1] + 1) until selectedIndex
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
|
} else {
|
||||||
|
// Just select itself
|
||||||
|
range = IntRange.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
range.forEach {
|
||||||
|
val inbetweenItem = get(it)
|
||||||
|
if (!inbetweenItem.selected) {
|
||||||
|
selectedMangaIds.add(inbetweenItem.libraryManga.manga.id)
|
||||||
|
set(it, inbetweenItem.copy(selected = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (userSelected && !fromLongPress) {
|
||||||
|
if (!selected) {
|
||||||
|
if (selectedIndex == selectedPositions[0]) {
|
||||||
|
selectedPositions[0] = indexOfFirst { it.selected }
|
||||||
|
} else if (selectedIndex == selectedPositions[1]) {
|
||||||
|
selectedPositions[1] = indexOfLast { it.selected }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedIndex < selectedPositions[0]) {
|
||||||
|
selectedPositions[0] = selectedIndex
|
||||||
|
} else if (selectedIndex > selectedPositions[1]) {
|
||||||
|
selectedPositions[1] = selectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleAllSelection(selected: Boolean) {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.map {
|
||||||
|
selectedMangaIds.addOrRemove(it.libraryManga.manga.id, selected)
|
||||||
|
it.copy(selected = selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String?) {
|
||||||
|
mutableState.update { it.copy(searchQuery = query) }
|
||||||
|
if (query != null) {
|
||||||
|
preferenceStore.getString("search_query", "").set(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMangas(libraryMangas: List<DownloadStatManga>) {
|
||||||
|
coroutineScope.launchNonCancellable {
|
||||||
|
libraryMangas.forEach { manga ->
|
||||||
|
val source = sourceManager.get(manga.libraryManga.manga.source) ?: return@forEach
|
||||||
|
downloadManager.deleteManga(manga.libraryManga.manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val set = libraryMangas.map { it.libraryManga.id }.toHashSet()
|
||||||
|
toggleAllSelection(false)
|
||||||
|
mutableState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
items = state.items.filterNot { it.libraryManga.id in set },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invertSelection() {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.map {
|
||||||
|
selectedMangaIds.addOrRemove(it.libraryManga.manga.id, !it.selected)
|
||||||
|
it.copy(selected = !it.selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupSelection(items: List<DownloadStatManga>) {
|
||||||
|
val newSelected = items.map { manga -> manga.libraryManga.id }.toHashSet()
|
||||||
|
selectedMangaIds.addAll(newSelected)
|
||||||
|
mutableState.update { state ->
|
||||||
|
val newItems = state.items.map {
|
||||||
|
it.copy(selected = if (it.libraryManga.id in newSelected) !it.selected else it.selected)
|
||||||
|
}
|
||||||
|
state.copy(items = newItems)
|
||||||
|
}
|
||||||
|
selectedPositions[0] = -1
|
||||||
|
selectedPositions[1] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GroupByMode {
|
||||||
|
NONE,
|
||||||
|
BY_CATEGORY,
|
||||||
|
BY_SOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SortingMode {
|
||||||
|
BY_ALPHABET,
|
||||||
|
BY_SIZE,
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package eu.kanade.presentation.more.download
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DownloadStatsScreenState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val items: List<DownloadStatManga> = emptyList(),
|
||||||
|
val groupByMode: GroupByMode = GroupByMode.NONE,
|
||||||
|
val sortMode: SortingMode = SortingMode.BY_ALPHABET,
|
||||||
|
val descendingOrder: Boolean = false,
|
||||||
|
val searchQuery: String? = null,
|
||||||
|
) {
|
||||||
|
val selected = items.filter { it.selected }
|
||||||
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
val processedItems: List<DownloadStatManga>
|
||||||
|
get() = search(items, searchQuery, groupByMode)
|
||||||
|
|
||||||
|
fun search(items: List<DownloadStatManga>, searchQuery: String?, groupByMode: GroupByMode): List<DownloadStatManga> {
|
||||||
|
return if (searchQuery != null) {
|
||||||
|
items.filter { downloadStatManga ->
|
||||||
|
downloadStatManga.libraryManga.manga.title.contains(searchQuery, true) ||
|
||||||
|
if (groupByMode == GroupByMode.BY_SOURCE) { downloadStatManga.source.name.contains(searchQuery, true) } else { false } ||
|
||||||
|
if (groupByMode == GroupByMode.BY_CATEGORY) { downloadStatManga.category.name.contains(searchQuery, true) } else { false }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
package eu.kanade.presentation.more.download
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.MaterialTheme.typography
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.util.fastAny
|
||||||
|
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||||
|
import eu.kanade.presentation.manga.components.MangaCover
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.util.selectedBackground
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
fun LazyListScope.downloadStatUiItems(
|
||||||
|
items: List<DownloadStatManga>,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onSelected: (DownloadStatManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
onClick: (DownloadStatManga) -> Unit,
|
||||||
|
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
) { item ->
|
||||||
|
DownloadStatUiItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
selected = item.selected,
|
||||||
|
onLongClick = {
|
||||||
|
onSelected(item, !item.selected, true, true)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
when {
|
||||||
|
selectionMode -> onSelected(item, !item.selected, true, false)
|
||||||
|
else -> onClick(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manga = item,
|
||||||
|
onDeleteManga = { onDeleteManga(listOf(item)) }.takeIf { !selectionMode },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DownloadStatUiItem(
|
||||||
|
modifier: Modifier,
|
||||||
|
manga: DownloadStatManga,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
onDeleteManga: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val textAlpha = 1f
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.selectedBackground(selected)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = {
|
||||||
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
MangaCover.Square(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
data = manga.libraryManga.manga,
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.medium)
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = manga.libraryManga.manga.title,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
var textHeight by remember { mutableIntStateOf(0) }
|
||||||
|
FolderSizeText(manga.folderSize)
|
||||||
|
DotSeparatorText()
|
||||||
|
Text(
|
||||||
|
text = String.format("%d %s", manga.downloadChaptersCount, stringResource(R.string.chapters)),
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalContentColor.current.copy(alpha = textAlpha),
|
||||||
|
onTextLayout = { textHeight = it.size.height },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadedIndicator(
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
onClick = { onDeleteManga?.invoke() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LazyListScope.downloadStatGroupUiItem(
|
||||||
|
items: List<DownloadStatManga>,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onSelected: (DownloadStatManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
onMangaClick: (DownloadStatManga) -> Unit,
|
||||||
|
id: String,
|
||||||
|
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||||
|
onGroupSelected: (List<DownloadStatManga>) -> Unit,
|
||||||
|
expanded: MutableMap<String, Boolean>,
|
||||||
|
) {
|
||||||
|
stickyHeader {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectedBackground(!items.fastAny { !it.selected })
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { expanded[id] = if (expanded[id] == null) { false } else { !expanded[id]!! } },
|
||||||
|
onLongClick = { onGroupSelected(items) },
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
id,
|
||||||
|
style = typography.h6,
|
||||||
|
)
|
||||||
|
DotSeparatorText()
|
||||||
|
FolderSizeText(items.fold(0L) { acc, downloadStatManga -> acc + downloadStatManga.folderSize })
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded[id] == true) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expanded[id] == true) {
|
||||||
|
downloadStatUiItems(
|
||||||
|
items = items,
|
||||||
|
onClick = onMangaClick,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
onDeleteManga = onDeleteManga,
|
||||||
|
onSelected = onSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FolderSizeText(folderSizeBytes: Long) {
|
||||||
|
val units = arrayOf(R.string.memory_unit_b, R.string.memory_unit_kb, R.string.memory_unit_mb, R.string.memory_unit_gb)
|
||||||
|
val base = 1024.0
|
||||||
|
val exponent = (ln(folderSizeBytes.toDouble()) / ln(base)).toInt()
|
||||||
|
val size = folderSizeBytes / base.pow(exponent.toDouble())
|
||||||
|
Text(
|
||||||
|
text = String.format("%.2f %s", size, stringResource(units[exponent])),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DownloadedIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.action_delete),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryList(
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
selectionMode: Boolean,
|
||||||
|
onMangaClick: (DownloadStatManga) -> Unit,
|
||||||
|
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||||
|
onGroupSelected: (List<DownloadStatManga>) -> Unit,
|
||||||
|
onSelected: (DownloadStatManga, Boolean, Boolean, Boolean) -> Unit,
|
||||||
|
categoryMap: Map<String, List<DownloadStatManga>>,
|
||||||
|
) {
|
||||||
|
val expanded = remember {
|
||||||
|
mutableStateMapOf(*categoryMap.keys.toList().map { it to false }.toTypedArray())
|
||||||
|
}
|
||||||
|
FastScrollLazyColumn(contentPadding = contentPadding) {
|
||||||
|
categoryMap.forEach { (category, items) ->
|
||||||
|
downloadStatGroupUiItem(
|
||||||
|
id = category,
|
||||||
|
items = items,
|
||||||
|
selectionMode = selectionMode,
|
||||||
|
onMangaClick = onMangaClick,
|
||||||
|
onSelected = onSelected,
|
||||||
|
onDeleteManga = onDeleteManga,
|
||||||
|
onGroupSelected = onGroupSelected,
|
||||||
|
expanded = expanded,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
|
|||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.presentation.more.MoreScreen
|
import eu.kanade.presentation.more.MoreScreen
|
||||||
|
import eu.kanade.presentation.more.download.DownloadStatsScreen
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
@ -74,6 +75,7 @@ object MoreTab : Tab {
|
|||||||
onClickDataAndStorage = { navigator.push(SettingsScreen.toDataAndStorageScreen()) },
|
onClickDataAndStorage = { navigator.push(SettingsScreen.toDataAndStorageScreen()) },
|
||||||
onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
|
onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
|
||||||
onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
|
onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
|
||||||
|
onClickDownloadState = { navigator.push(DownloadStatsScreen()) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
<string name="label_backup">Backup and restore</string>
|
<string name="label_backup">Backup and restore</string>
|
||||||
<string name="label_data_storage">Data and storage</string>
|
<string name="label_data_storage">Data and storage</string>
|
||||||
<string name="label_stats">Statistics</string>
|
<string name="label_stats">Statistics</string>
|
||||||
|
<string name="label_download_stats">Download statistics</string>
|
||||||
<string name="label_migration">Migrate</string>
|
<string name="label_migration">Migrate</string>
|
||||||
<string name="label_extensions">Extensions</string>
|
<string name="label_extensions">Extensions</string>
|
||||||
<string name="label_extension_info">Extension info</string>
|
<string name="label_extension_info">Extension info</string>
|
||||||
@ -66,6 +67,7 @@
|
|||||||
<string name="action_sort_latest_chapter">Latest chapter</string>
|
<string name="action_sort_latest_chapter">Latest chapter</string>
|
||||||
<string name="action_sort_chapter_fetch_date">Chapter fetch date</string>
|
<string name="action_sort_chapter_fetch_date">Chapter fetch date</string>
|
||||||
<string name="action_sort_date_added">Date added</string>
|
<string name="action_sort_date_added">Date added</string>
|
||||||
|
<string name="action_sort_size">Size</string>
|
||||||
<string name="action_search">Search</string>
|
<string name="action_search">Search</string>
|
||||||
<string name="action_search_hint">Search…</string>
|
<string name="action_search_hint">Search…</string>
|
||||||
<string name="action_search_settings">Search settings</string>
|
<string name="action_search_settings">Search settings</string>
|
||||||
@ -107,6 +109,9 @@
|
|||||||
<string name="action_open_in_browser">Open in browser</string>
|
<string name="action_open_in_browser">Open in browser</string>
|
||||||
<string name="action_show_manga">Show entry</string>
|
<string name="action_show_manga">Show entry</string>
|
||||||
<string name="action_copy_to_clipboard">Copy to clipboard</string>
|
<string name="action_copy_to_clipboard">Copy to clipboard</string>
|
||||||
|
<string name="action_group_by_category">Category</string>
|
||||||
|
<string name="action_group_by_source">Source</string>
|
||||||
|
<string name="action_ungroup">ungroup</string>
|
||||||
<!-- Do not translate "WebView" -->
|
<!-- Do not translate "WebView" -->
|
||||||
<string name="action_open_in_web_view">Open in WebView</string>
|
<string name="action_open_in_web_view">Open in WebView</string>
|
||||||
<string name="action_web_view" translatable="false">WebView</string>
|
<string name="action_web_view" translatable="false">WebView</string>
|
||||||
@ -131,6 +136,7 @@
|
|||||||
<string name="action_cancel_all">Cancel all</string>
|
<string name="action_cancel_all">Cancel all</string>
|
||||||
<string name="cancel_all_for_series">Cancel all for this series</string>
|
<string name="cancel_all_for_series">Cancel all for this series</string>
|
||||||
<string name="action_sort">Sort</string>
|
<string name="action_sort">Sort</string>
|
||||||
|
<string name="action_group">Group</string>
|
||||||
<string name="action_order_by_upload_date">By upload date</string>
|
<string name="action_order_by_upload_date">By upload date</string>
|
||||||
<string name="action_order_by_chapter_number">By chapter number</string>
|
<string name="action_order_by_chapter_number">By chapter number</string>
|
||||||
<string name="action_newest">Newest</string>
|
<string name="action_newest">Newest</string>
|
||||||
@ -158,6 +164,13 @@
|
|||||||
<string name="action_faq_and_guides">FAQ and Guides</string>
|
<string name="action_faq_and_guides">FAQ and Guides</string>
|
||||||
<string name="action_not_now">Not now</string>
|
<string name="action_not_now">Not now</string>
|
||||||
|
|
||||||
|
<!-- Memory units -->
|
||||||
|
<string name="memory_unit_b">B</string>
|
||||||
|
<string name="memory_unit_kb">KB</string>
|
||||||
|
<string name="memory_unit_mb">MB</string>
|
||||||
|
<string name="memory_unit_gb">GB</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Operations -->
|
<!-- Operations -->
|
||||||
<string name="loading">Loading…</string>
|
<string name="loading">Loading…</string>
|
||||||
<string name="internal_error">InternalError: Check crash logs for further information</string>
|
<string name="internal_error">InternalError: Check crash logs for further information</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user