mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 14:39:15 +01:00
many changes for download stat screen
This commit is contained in:
parent
cc4e994682
commit
7f777e5c1a
@ -26,6 +26,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
|
||||
@ -69,6 +70,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
|
||||
@ -161,5 +165,9 @@ class DomainModule : InjektModule {
|
||||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleSource(get()) }
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
|
||||
addSingletonFactory<DownloadStatRepository> { DownloadStatRepositoryImpl(get()) }
|
||||
addFactory { GetDownloadStatOperations(get()) }
|
||||
addFactory { AddDownloadStatOperation(get()) }
|
||||
}
|
||||
}
|
||||
|
@ -100,5 +100,6 @@ data class TabContent(
|
||||
val badgeNumber: Int? = null,
|
||||
val searchEnabled: Boolean = false,
|
||||
val actions: List<AppBar.Action> = emptyList(),
|
||||
val actionModeActions: List<AppBar.Action> = emptyList(),
|
||||
val content: @Composable (contentPadding: PaddingValues, snackbarHostState: SnackbarHostState) -> Unit,
|
||||
)
|
||||
|
@ -1,26 +1,30 @@
|
||||
package eu.kanade.presentation.more.download
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
@ -28,15 +32,23 @@ 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.more.download.components.DeleteAlertDialog
|
||||
import eu.kanade.presentation.more.download.components.DownloadStatOperationInfoDialog
|
||||
import eu.kanade.presentation.more.download.components.DownloadStatOperationInfoStartDialog
|
||||
import eu.kanade.presentation.more.download.components.DownloadStatOperationMultiInfoDialog
|
||||
import eu.kanade.presentation.more.download.components.DownloadStatSettingsDialog
|
||||
import eu.kanade.presentation.more.download.data.DownloadStatManga
|
||||
import eu.kanade.presentation.more.download.tabs.downloadStatsItemsTab
|
||||
import eu.kanade.presentation.more.download.tabs.overAllStatsTab
|
||||
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 kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.components.HorizontalPager
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||
import tachiyomi.presentation.core.components.material.TabText
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class DownloadStatsScreen : Screen() {
|
||||
@ -47,85 +59,131 @@ class DownloadStatsScreen : Screen() {
|
||||
|
||||
val screenModel = rememberScreenModel { DownloadStatsScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val tabs = listOf(
|
||||
overAllStatsTab(
|
||||
state = state,
|
||||
screenModel = screenModel,
|
||||
),
|
||||
downloadStatsItemsTab(
|
||||
state = state,
|
||||
screenModel = screenModel,
|
||||
),
|
||||
)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pagerState = rememberPagerState(initialPage = screenModel.activeCategoryIndex) { tabs.size }
|
||||
|
||||
val tab = tabs[pagerState.currentPage]
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is Dialog.SettingsSheet -> run {
|
||||
DownloadStatSettingsDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
descendingOrder = state.descendingOrder,
|
||||
sortMode = state.sortMode,
|
||||
groupByMode = state.groupByMode,
|
||||
showNotDownloaded = state.showNotDownloaded,
|
||||
onSort = screenModel::runSort,
|
||||
onGroup = screenModel::runGroupBy,
|
||||
toggleShowNotDownloaded = screenModel::toggleShowNoDownload,
|
||||
)
|
||||
}
|
||||
is Dialog.DeleteManga -> {
|
||||
DeleteAlertDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onConfirm = { screenModel.deleteMangas(dialog.items) },
|
||||
)
|
||||
}
|
||||
is Dialog.DownloadStatOperationInfo -> {
|
||||
DownloadStatOperationInfoDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onMangaClick = { mangaId -> navigator.push(MangaScreen(mangaId)) },
|
||||
item = dialog.downloadStatOperation,
|
||||
findManga = screenModel::findManga,
|
||||
)
|
||||
}
|
||||
|
||||
is Dialog.MultiMangaDownloadStatOperationInfo -> {
|
||||
DownloadStatOperationMultiInfoDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onMangaClick = { mangaId -> navigator.push(MangaScreen(mangaId)) },
|
||||
items = dialog.downloadStatOperation,
|
||||
findManga = screenModel::findManga,
|
||||
)
|
||||
}
|
||||
|
||||
is Dialog.DownloadStatOperationStart -> {
|
||||
DownloadStatOperationInfoStartDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
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,
|
||||
defaultActions = tab.actions,
|
||||
actionModeActions = tab.actionModeActions,
|
||||
searchEnabled = tab.searchEnabled,
|
||||
actionModeEnabled = tab.actionModeActions.isNotEmpty(),
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { 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),
|
||||
if (state.isLoading) {
|
||||
LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
top = contentPadding.calculateTopPadding(),
|
||||
start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
),
|
||||
) {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
indicator = {
|
||||
TabIndicator(
|
||||
it[pagerState.currentPage],
|
||||
pagerState.currentPageOffsetFraction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
tabs.forEachIndexed { index, tab ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { TabText(text = stringResource(tab.titleRes)) },
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalPager(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { page ->
|
||||
tabs[page].content(
|
||||
PaddingValues(
|
||||
bottom = contentPadding.calculateBottomPadding(),
|
||||
),
|
||||
snackbarHostState,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
screenModel.activeCategoryIndex = pagerState.currentPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,32 +192,27 @@ class DownloadStatsScreen : Screen() {
|
||||
|
||||
@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)?,
|
||||
defaultActions: List<AppBar.Action>,
|
||||
actionModeActions: List<AppBar.Action>,
|
||||
searchEnabled: Boolean,
|
||||
actionModeEnabled: Boolean,
|
||||
) {
|
||||
if (selected.isNotEmpty()) {
|
||||
DownloadStatsActionAppBar(
|
||||
if (actionModeEnabled && selected.isNotEmpty()) {
|
||||
AppBar(
|
||||
modifier = modifier,
|
||||
selected = selected,
|
||||
onSelectAll = onSelectAll,
|
||||
onInvertSelection = onInvertSelection,
|
||||
title = stringResource(R.string.label_download_stats),
|
||||
onCancelActionMode = onCancelActionMode,
|
||||
actionModeActions = { AppBarActions(actionModeActions) },
|
||||
actionModeCounter = selected.size,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigateUp = navigateUp,
|
||||
onMultiDeleteClicked = onMultiDeleteClicked,
|
||||
)
|
||||
BackHandler(
|
||||
onBack = onCancelActionMode,
|
||||
@ -177,143 +230,16 @@ private fun DownloadStatsAppBar(
|
||||
)
|
||||
}
|
||||
},
|
||||
searchQuery = searchQuery,
|
||||
searchEnabled = searchEnabled,
|
||||
searchQuery = searchQuery.takeIf { searchEnabled },
|
||||
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 },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
actions = { AppBarActions(defaultActions) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
if (searchQuery != null) {
|
||||
if (searchQuery != null && searchEnabled) {
|
||||
BackHandler(
|
||||
onBack = { onChangeSearchQuery(null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
actionModeActions = {
|
||||
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) },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
actionModeCounter = selected.size,
|
||||
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_A_Z),
|
||||
SortingMode.BY_SIZE to stringResource(R.string.action_sort_size),
|
||||
).map { (mode, string) ->
|
||||
SortItem(
|
||||
label = string,
|
||||
sortDescending = descendingOrder.takeIf { sortState == mode },
|
||||
onClick = { onSortClicked(mode) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,38 @@
|
||||
package eu.kanade.presentation.more.download
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.presentation.more.download.components.graphic.GraphGroupByMode
|
||||
import eu.kanade.presentation.more.download.components.graphic.GraphicPoint
|
||||
import eu.kanade.presentation.more.download.data.DownloadStatManga
|
||||
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 eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.preference.toggle
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TreeMap
|
||||
|
||||
class DownloadStatsScreenModel(
|
||||
@ -30,87 +42,71 @@ class DownloadStatsScreenModel(
|
||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
private val getDownloadStatOperations: GetDownloadStatOperations = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) : StateScreenModel<DownloadStatsScreenState>(DownloadStatsScreenState()) {
|
||||
|
||||
private val downloadCache: DownloadCache by injectLazy()
|
||||
var activeCategoryIndex: Int by preferenceStore.getInt("downloadStatSelectedTab", 0).asState(coroutineScope)
|
||||
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
private val selectedMangaIds: HashSet<Long> = HashSet()
|
||||
private lateinit var lastSelectedManga: LibraryManga
|
||||
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
val sortMode = preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).get()
|
||||
mutableState.update {
|
||||
mutableState.update { state ->
|
||||
val categories = getCategories.await().associateBy { group -> group.id }
|
||||
it.copy(
|
||||
val operations = getDownloadStatOperations.await()
|
||||
state.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]!!,
|
||||
)
|
||||
(downloadManager.getDownloadCount(libraryManga.manga) > 0) || operations.any { it.mangaId == libraryManga.id }
|
||||
}.mapNotNull { libraryManga ->
|
||||
val source = sourceManager.getOrStub(libraryManga.manga.source)
|
||||
val path = downloadProvider.findMangaDir(
|
||||
libraryManga.manga.title,
|
||||
source,
|
||||
)?.filePath
|
||||
val downloadChaptersCount = downloadManager.getDownloadCount(libraryManga.manga)
|
||||
if (downloadChaptersCount == 0) {
|
||||
DownloadStatManga(
|
||||
libraryManga = libraryManga,
|
||||
source = source,
|
||||
category = categories[libraryManga.category]!!,
|
||||
)
|
||||
} else if (path != null) {
|
||||
DownloadStatManga(
|
||||
libraryManga = libraryManga,
|
||||
source = source,
|
||||
folderSize = downloadProvider.getFolderSize(path),
|
||||
downloadChaptersCount = downloadChaptersCount,
|
||||
category = categories[libraryManga.category]!!,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
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 != "" },
|
||||
downloadStatOperations = operations,
|
||||
showNotDownloaded = preferenceStore.getBoolean("show_no_downloaded", false).get(),
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
runSortAction(sortMode)
|
||||
runSort(sortMode, true)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return if (file.isDirectory) {
|
||||
getFolderSize(file.path)
|
||||
} else if (file.isFile) {
|
||||
file.length()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun runSortAction(mode: SortingMode) {
|
||||
fun runSort(
|
||||
mode: SortingMode,
|
||||
initSort: Boolean = false,
|
||||
) {
|
||||
when (mode) {
|
||||
SortingMode.BY_ALPHABET -> sortByAlphabet()
|
||||
SortingMode.BY_SIZE -> sortBySize()
|
||||
SortingMode.BY_ALPHABET -> sortByAlphabet(initSort)
|
||||
SortingMode.BY_SIZE -> sortBySize(initSort)
|
||||
SortingMode.BY_CHAPTERS -> sortByChapters(initSort)
|
||||
}
|
||||
preferenceStore.getEnum("sort_mode", SortingMode.BY_ALPHABET).set(mode)
|
||||
}
|
||||
|
||||
fun runGroupBy(mode: GroupByMode) {
|
||||
@ -119,11 +115,21 @@ class DownloadStatsScreenModel(
|
||||
GroupByMode.BY_CATEGORY -> groupByCategory()
|
||||
GroupByMode.BY_SOURCE -> groupBySource()
|
||||
}
|
||||
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(mode)
|
||||
}
|
||||
|
||||
private fun sortByAlphabet() {
|
||||
fun toggleShowNoDownload() {
|
||||
val showNotDownloaded = preferenceStore.getBoolean("show_no_downloaded").toggle()
|
||||
mutableState.update { state ->
|
||||
val descendingOrder = if (state.sortMode == SortingMode.BY_ALPHABET) !state.descendingOrder else false
|
||||
state.copy(
|
||||
showNotDownloaded = showNotDownloaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortByAlphabet(initSort: Boolean) {
|
||||
mutableState.update { state ->
|
||||
val descendingOrder = if (initSort) state.descendingOrder else 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 },
|
||||
@ -131,13 +137,43 @@ class DownloadStatsScreenModel(
|
||||
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>> {
|
||||
private fun sortBySize(initSort: Boolean) {
|
||||
mutableState.update { state ->
|
||||
val descendingOrder = if (initSort) state.descendingOrder else 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortByChapters(initSort: Boolean) {
|
||||
mutableState.update { state ->
|
||||
val descendingOrder = if (initSort) state.descendingOrder else if (state.sortMode == SortingMode.BY_CHAPTERS) !state.descendingOrder else false
|
||||
preferenceStore.getBoolean("descending_order", false).set(descendingOrder)
|
||||
state.copy(
|
||||
items = if (descendingOrder) state.items.sortedByDescending { it.downloadChaptersCount } else state.items.sortedBy { it.downloadChaptersCount },
|
||||
descendingOrder = descendingOrder,
|
||||
sortMode = SortingMode.BY_CHAPTERS,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun categoryMap(
|
||||
items: List<DownloadStatManga>,
|
||||
groupMode: GroupByMode,
|
||||
sortMode: SortingMode,
|
||||
descendingOrder: Boolean,
|
||||
defaultCategoryName: String?,
|
||||
): 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_CATEGORY -> items.groupBy {
|
||||
if (it.category.isSystemCategory && defaultCategoryName != null) { defaultCategoryName } else { it.category.name }
|
||||
}
|
||||
GroupByMode.BY_SOURCE -> items.groupBy { it.source.name }
|
||||
GroupByMode.NONE -> emptyMap()
|
||||
}
|
||||
@ -153,29 +189,21 @@ class DownloadStatsScreenModel(
|
||||
sortedMap.putAll(unsortedMap)
|
||||
sortedMap
|
||||
}
|
||||
SortingMode.BY_CHAPTERS -> {
|
||||
val compareFun: (String) -> Comparable<*> = { it: String -> unsortedMap[it]?.sumOf { manga -> manga.downloadChaptersCount } ?: 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() {
|
||||
@ -184,7 +212,6 @@ class DownloadStatsScreenModel(
|
||||
groupByMode = GroupByMode.BY_CATEGORY,
|
||||
)
|
||||
}
|
||||
preferenceStore.getEnum("group_by_mode", GroupByMode.NONE).set(GroupByMode.BY_CATEGORY)
|
||||
}
|
||||
|
||||
private fun unGroup() {
|
||||
@ -193,67 +220,66 @@ class DownloadStatsScreenModel(
|
||||
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,
|
||||
) {
|
||||
lastSelectedManga = item.libraryManga
|
||||
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
|
||||
state.copy(
|
||||
items = state.items.map {
|
||||
if (it.libraryManga.id == item.libraryManga.id) {
|
||||
it.copy(selected = !it.selected)
|
||||
} 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
|
||||
}
|
||||
it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
fun toggleMassSelection(item: DownloadStatManga) {
|
||||
mutableState.update { state ->
|
||||
val processedItems = if (state.groupByMode == GroupByMode.NONE) {
|
||||
state.processedItems(false)
|
||||
} else {
|
||||
val temp = mutableListOf<DownloadStatManga>()
|
||||
categoryMap(
|
||||
items = state.processedItems(false),
|
||||
groupMode = state.groupByMode,
|
||||
sortMode = state.sortMode,
|
||||
descendingOrder = state.descendingOrder,
|
||||
defaultCategoryName = null,
|
||||
).map {
|
||||
temp.addAll(it.value)
|
||||
}
|
||||
temp
|
||||
}
|
||||
val lastSelectedIndex =
|
||||
processedItems.indexOfFirst { it.libraryManga.id == lastSelectedManga.id && it.libraryManga.category == lastSelectedManga.category }
|
||||
val selectedIndex =
|
||||
processedItems.indexOfFirst { it.libraryManga.id == item.libraryManga.id && it.libraryManga.category == item.libraryManga.category }
|
||||
val itemsToChange = mutableSetOf(processedItems[lastSelectedIndex].libraryManga.id)
|
||||
val range = if (selectedIndex < lastSelectedIndex) {
|
||||
selectedIndex until lastSelectedIndex
|
||||
} else if (selectedIndex > lastSelectedIndex) {
|
||||
(lastSelectedIndex + 1) until (selectedIndex + 1)
|
||||
} else {
|
||||
IntRange.EMPTY
|
||||
}
|
||||
range.forEach {
|
||||
val betweenItem = processedItems[it]
|
||||
if (!betweenItem.selected) {
|
||||
lastSelectedManga = betweenItem.libraryManga
|
||||
itemsToChange.add(betweenItem.libraryManga.id)
|
||||
}
|
||||
}
|
||||
val newItems = state.items.map {
|
||||
if (it.libraryManga.id in itemsToChange) {
|
||||
it.copy(selected = true)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
@ -262,15 +288,45 @@ class DownloadStatsScreenModel(
|
||||
|
||||
fun toggleAllSelection(selected: Boolean) {
|
||||
mutableState.update { state ->
|
||||
val newSelected = state.processedItems(false).map { manga -> manga.libraryManga.id }.toHashSet()
|
||||
val newItems = state.items.map {
|
||||
selectedMangaIds.addOrRemove(it.libraryManga.manga.id, selected)
|
||||
it.copy(selected = selected)
|
||||
if (it.libraryManga.id in newSelected) {
|
||||
it.copy(selected = selected)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
fun invertSelection() {
|
||||
mutableState.update { state ->
|
||||
val newSelected = state.processedItems(false).map { manga -> manga.libraryManga.id }.toHashSet()
|
||||
val newItems = state.items.map {
|
||||
if (it.libraryManga.id in newSelected) {
|
||||
it.copy(selected = !it.selected)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
fun groupSelection(items: List<DownloadStatManga>) {
|
||||
val newSelected = items.map { manga -> manga.libraryManga.manga.id }.toHashSet()
|
||||
lastSelectedManga = items.last().libraryManga
|
||||
mutableState.update { state ->
|
||||
val newItems = state.items.map {
|
||||
if (it.libraryManga.manga.id in newSelected) {
|
||||
it.copy(selected = !it.selected)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
state.copy(items = newItems)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
@ -281,46 +337,143 @@ class DownloadStatsScreenModel(
|
||||
preferenceStore.getString("search_query", "").delete()
|
||||
}
|
||||
}
|
||||
fun findManga(id: Long?): Manga? {
|
||||
return runBlocking { if (id != null) getManga.await(id) else null }
|
||||
}
|
||||
|
||||
fun deleteMangas(libraryMangas: List<DownloadStatManga>) {
|
||||
fun deleteMangas(manga: List<DownloadStatManga>) {
|
||||
coroutineScope.launchNonCancellable {
|
||||
libraryMangas.forEach { manga ->
|
||||
val source = sourceManager.get(manga.libraryManga.manga.source) ?: return@forEach
|
||||
downloadManager.deleteManga(manga.libraryManga.manga, source)
|
||||
manga.forEach { manga ->
|
||||
downloadManager.deleteManga(manga.libraryManga.manga, manga.source)
|
||||
}
|
||||
}
|
||||
val set = libraryMangas.map { it.libraryManga.id }.toHashSet()
|
||||
val toDeleteIds = manga.map { it.libraryManga.manga.id }.toHashSet()
|
||||
toggleAllSelection(false)
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
items = state.items.filterNot { it.libraryManga.id in set },
|
||||
items = state.items.map { if (it.libraryManga.manga.id in toDeleteIds) it.copy(downloadChaptersCount = 0, folderSize = 0) else it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 showDeleteAlert(items: List<DownloadStatManga>) {
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteManga(items)) }
|
||||
}
|
||||
|
||||
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)
|
||||
fun dismissDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun openSettingsDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.SettingsSheet) }
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update { it.copy(dialog = dialog) }
|
||||
}
|
||||
|
||||
fun formGraphData(
|
||||
downloadStatOperations: List<DownloadStatOperation>,
|
||||
currentWeight: Long,
|
||||
graphGroupByMode: GraphGroupByMode,
|
||||
context: Context,
|
||||
): List<GraphicPoint> {
|
||||
var weight = currentWeight.toFloat()
|
||||
val pointsList = mutableListOf<GraphicPoint>()
|
||||
for (i in downloadStatOperations.indices.reversed()) {
|
||||
weight -= downloadStatOperations[i].size
|
||||
}
|
||||
pointsList.add(
|
||||
GraphicPoint(
|
||||
coordinate = weight,
|
||||
subLine = context.getString(R.string.graph_start_point_subLine),
|
||||
dialog = Dialog.DownloadStatOperationStart,
|
||||
),
|
||||
)
|
||||
when (graphGroupByMode) {
|
||||
GraphGroupByMode.NONE -> {
|
||||
for (i in downloadStatOperations) {
|
||||
weight += i.size
|
||||
pointsList.add(
|
||||
GraphicPoint(
|
||||
coordinate = weight,
|
||||
subLine = Date(i.date).toRelativeString(
|
||||
context = context,
|
||||
range = 0,
|
||||
),
|
||||
dialog = Dialog.DownloadStatOperationInfo(i),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
GraphGroupByMode.BY_DAY -> {
|
||||
val dateMap = mutableMapOf<String, MutableList<DownloadStatOperation>>()
|
||||
for (i in downloadStatOperations) {
|
||||
weight += i.size
|
||||
val key = Date(i.date).toRelativeString(
|
||||
context = context,
|
||||
range = 0,
|
||||
)
|
||||
if (dateMap.containsKey(key)) {
|
||||
dateMap[key]?.add(i)
|
||||
} else {
|
||||
dateMap[key] = mutableListOf(i)
|
||||
}
|
||||
}
|
||||
for (i in dateMap) {
|
||||
weight += i.value.sumOf { it.size }
|
||||
pointsList.add(
|
||||
GraphicPoint(
|
||||
coordinate = weight,
|
||||
subLine = i.key,
|
||||
dialog = Dialog.MultiMangaDownloadStatOperationInfo(
|
||||
i.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
GraphGroupByMode.BY_MONTH -> {
|
||||
val dateMap = mutableMapOf<String, MutableList<DownloadStatOperation>>()
|
||||
val dateFormat = SimpleDateFormat("MM.yyyy", Locale.US)
|
||||
for (i in downloadStatOperations) {
|
||||
weight += i.size
|
||||
val key = dateFormat.format(Date(i.date))
|
||||
if (dateMap.containsKey(key)) {
|
||||
dateMap[key]?.add(i)
|
||||
} else {
|
||||
dateMap[key] = mutableListOf(i)
|
||||
}
|
||||
}
|
||||
for (i in dateMap) {
|
||||
weight += i.value.sumOf { it.size }
|
||||
pointsList.add(
|
||||
GraphicPoint(
|
||||
coordinate = weight,
|
||||
subLine = i.key,
|
||||
dialog = Dialog.MultiMangaDownloadStatOperationInfo(
|
||||
i.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return pointsList
|
||||
}
|
||||
|
||||
fun toggleExpanded(manga: DownloadStatManga) {
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
items = state.items.map {
|
||||
if (it.libraryManga.id == manga.libraryManga.id && it.libraryManga.category == manga.libraryManga.category) {
|
||||
it.copy(expanded = !it.expanded)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
selectedPositions[0] = -1
|
||||
selectedPositions[1] = -1
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,4 +486,13 @@ enum class GroupByMode {
|
||||
enum class SortingMode {
|
||||
BY_ALPHABET,
|
||||
BY_SIZE,
|
||||
BY_CHAPTERS,
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class DeleteManga(val items: List<DownloadStatManga>) : Dialog()
|
||||
data class DownloadStatOperationInfo(val downloadStatOperation: DownloadStatOperation) : Dialog()
|
||||
data class MultiMangaDownloadStatOperationInfo(val downloadStatOperation: List<DownloadStatOperation>) : Dialog()
|
||||
object DownloadStatOperationStart : Dialog()
|
||||
object SettingsSheet : Dialog()
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.presentation.more.download
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.presentation.more.download.data.DownloadStatManga
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
|
||||
@Immutable
|
||||
data class DownloadStatsScreenState(
|
||||
@ -10,21 +12,35 @@ data class DownloadStatsScreenState(
|
||||
val sortMode: SortingMode = SortingMode.BY_ALPHABET,
|
||||
val descendingOrder: Boolean = false,
|
||||
val searchQuery: String? = null,
|
||||
val showNotDownloaded: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
val downloadStatOperations: List<DownloadStatOperation> = emptyList(),
|
||||
) {
|
||||
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 }
|
||||
fun uniqueItems(): List<DownloadStatManga> {
|
||||
val uniqueIds = HashSet<Long>()
|
||||
val uniqueMangas = mutableListOf<DownloadStatManga>()
|
||||
for (manga in items) {
|
||||
if (uniqueIds.add(manga.libraryManga.manga.id)) {
|
||||
uniqueMangas.add(manga)
|
||||
}
|
||||
}
|
||||
return uniqueMangas
|
||||
}
|
||||
|
||||
fun processedItems(unique: Boolean): List<DownloadStatManga> {
|
||||
return (if (unique) uniqueItems() else items).filter {
|
||||
if (!showNotDownloaded) it.downloadChaptersCount > 0 else true
|
||||
}.filter {
|
||||
if (searchQuery != null) {
|
||||
it.libraryManga.manga.title.contains(searchQuery, true) ||
|
||||
if (groupByMode == GroupByMode.BY_SOURCE) { it.source.name.contains(searchQuery, true) } else { false } ||
|
||||
if (groupByMode == GroupByMode.BY_CATEGORY) { it.category.name.contains(searchQuery, true) } else { false }
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,265 +0,0 @@
|
||||
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.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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 categoryExpandedMapSaver: Saver<MutableMap<String, Boolean>, *> = Saver(
|
||||
save = { map -> map.toMap() },
|
||||
restore = { map -> mutableStateMapOf(*map.toList().toTypedArray()) },
|
||||
)
|
||||
|
||||
val expanded = rememberSaveable(
|
||||
saver = categoryExpandedMapSaver,
|
||||
key = "CategoryExpandedMap",
|
||||
init = { 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
package eu.kanade.presentation.more.download.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.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 androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.manga.components.MangaCover
|
||||
import eu.kanade.presentation.more.download.GroupByMode
|
||||
import eu.kanade.presentation.more.download.SortingMode
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.LazyColumn
|
||||
import tachiyomi.presentation.core.components.RadioItem
|
||||
import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun DeleteAlertDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.confirm_delete_entries))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadStatOperationInfoDialog(
|
||||
findManga: (Long?) -> Manga?,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
item: DownloadStatOperation,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
val manga = findManga(item.mangaId)
|
||||
if (manga != null) {
|
||||
MangaOperationsRow(
|
||||
manga = manga,
|
||||
onMangaClick = { onMangaClick(manga.id) },
|
||||
items = listOf(item),
|
||||
)
|
||||
} else {
|
||||
NoMangaOperationsRow(listOf(item))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadStatOperationMultiInfoDialog(
|
||||
findManga: (Long?) -> Manga?,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
items: List<DownloadStatOperation>,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
LazyColumn {
|
||||
items(items = items.groupBy { it.mangaId }.values.toList()) { items ->
|
||||
val manga = findManga(items.first().mangaId)
|
||||
if (manga != null) {
|
||||
MangaOperationsRow(
|
||||
manga = manga,
|
||||
onMangaClick = { onMangaClick(manga.id) },
|
||||
items = items,
|
||||
)
|
||||
} else {
|
||||
NoMangaOperationsRow(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaOperationsRow(
|
||||
manga: Manga,
|
||||
onMangaClick: () -> Unit,
|
||||
items: List<DownloadStatOperation>,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(120.dp),
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.fillMaxHeight(),
|
||||
data = manga,
|
||||
onClick = onMangaClick,
|
||||
)
|
||||
MangaInfoColumn(items, manga.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaInfoColumn(
|
||||
items: List<DownloadStatOperation>,
|
||||
title: String,
|
||||
) {
|
||||
val downloadItems = items.filter { it.size > 0 }
|
||||
val deleteItems = items.filter { it.size < 0 }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = LocalContentColor.current.copy(alpha = 1f),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (downloadItems.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
Text(
|
||||
text = String.format(
|
||||
stringResource(R.string.download_stat_operation_downloaded),
|
||||
downloadItems.sumOf { it.units },
|
||||
folderSizeText(
|
||||
folderSizeBytes = downloadItems.sumOf { it.size },
|
||||
),
|
||||
),
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalContentColor.current.copy(alpha = 1f),
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (deleteItems.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
Text(
|
||||
text = String.format(
|
||||
stringResource(R.string.download_stat_operation_deleted),
|
||||
deleteItems.sumOf { it.units },
|
||||
folderSizeText(
|
||||
folderSizeBytes = abs(deleteItems.sumOf { it.size }),
|
||||
),
|
||||
),
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalContentColor.current.copy(alpha = 1f),
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoMangaOperationsRow(
|
||||
items: List<DownloadStatOperation>,
|
||||
) {
|
||||
Row {
|
||||
MangaInfoColumn(items, stringResource(R.string.entry_was_deleted))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadStatOperationInfoStartDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.start_operation_massage))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadStatSettingsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
sortMode: SortingMode,
|
||||
descendingOrder: Boolean,
|
||||
groupByMode: GroupByMode,
|
||||
showNotDownloaded: Boolean,
|
||||
onSort: (SortingMode) -> Unit,
|
||||
onGroup: (GroupByMode) -> Unit,
|
||||
toggleShowNotDownloaded: () -> Unit,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
tabTitles = listOf(
|
||||
stringResource(R.string.action_sort),
|
||||
stringResource(R.string.action_group),
|
||||
stringResource(R.string.action_settings),
|
||||
),
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> SortPage(
|
||||
onSort = onSort,
|
||||
descendingOrder = descendingOrder,
|
||||
sortMode = sortMode,
|
||||
|
||||
)
|
||||
1 -> GroupPage(
|
||||
onGroup = onGroup,
|
||||
groupByMode = groupByMode,
|
||||
)
|
||||
2 -> SettingPage(
|
||||
toggleShowNotDownloaded = toggleShowNotDownloaded,
|
||||
showNotDownloaded = showNotDownloaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SortPage(
|
||||
onSort: (SortingMode) -> Unit,
|
||||
descendingOrder: Boolean,
|
||||
sortMode: SortingMode,
|
||||
) {
|
||||
listOfNotNull(
|
||||
SortingMode.BY_ALPHABET to stringResource(R.string.action_sort_alpha),
|
||||
SortingMode.BY_SIZE to stringResource(R.string.action_sort_size),
|
||||
SortingMode.BY_CHAPTERS to stringResource(R.string.chapters),
|
||||
).map { (mode, string) ->
|
||||
SortItem(
|
||||
label = string,
|
||||
sortDescending = descendingOrder.takeIf { sortMode == mode },
|
||||
onClick = { onSort(mode) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupPage(
|
||||
onGroup: (GroupByMode) -> Unit,
|
||||
groupByMode: GroupByMode,
|
||||
) {
|
||||
listOf(
|
||||
GroupByMode.NONE to stringResource(R.string.action_ungroup),
|
||||
GroupByMode.BY_CATEGORY to stringResource(R.string.action_group_by_category),
|
||||
GroupByMode.BY_SOURCE to stringResource(R.string.action_group_by_source),
|
||||
).map { (mode, string) ->
|
||||
RadioItem(
|
||||
label = string,
|
||||
selected = groupByMode == mode,
|
||||
onClick = { onGroup(mode) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingPage(
|
||||
toggleShowNotDownloaded: () -> Unit,
|
||||
showNotDownloaded: Boolean,
|
||||
) {
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.show_entries_without_downloaded),
|
||||
checked = showNotDownloaded,
|
||||
onClick = toggleShowNotDownloaded,
|
||||
)
|
||||
}
|
@ -0,0 +1,475 @@
|
||||
package eu.kanade.presentation.more.download.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.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.SnackbarHostState
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.presentation.more.download.components.graphic.GraphGroupByMode
|
||||
import eu.kanade.presentation.more.download.components.graphic.GraphicPoint
|
||||
import eu.kanade.presentation.more.download.components.graphic.PointGraph
|
||||
import eu.kanade.presentation.more.download.data.DownloadStatManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.format
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.selectedBackground
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
@Composable
|
||||
fun CategoryList(
|
||||
contentPadding: PaddingValues,
|
||||
selectionMode: Boolean,
|
||||
onCoverClick: (DownloadStatManga) -> Unit,
|
||||
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||
onGroupSelected: (List<DownloadStatManga>) -> Unit,
|
||||
onSelected: (DownloadStatManga) -> Unit,
|
||||
onMassSelected: (DownloadStatManga) -> Unit,
|
||||
categoryMap: Map<String, List<DownloadStatManga>>,
|
||||
toggleExpanded: (DownloadStatManga) -> Unit,
|
||||
operations: List<DownloadStatOperation>,
|
||||
getGraphPoints: (List<DownloadStatOperation>, Long, GraphGroupByMode) -> List<GraphicPoint>,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val categoryExpandedMapSaver: Saver<MutableMap<String, Boolean>, *> = Saver(
|
||||
save = { map -> map.toMap() },
|
||||
restore = { map -> mutableStateMapOf(*map.toList().toTypedArray()) },
|
||||
)
|
||||
|
||||
val expanded = rememberSaveable(
|
||||
saver = categoryExpandedMapSaver,
|
||||
init = { mutableStateMapOf(*categoryMap.keys.toList().map { it to false }.toTypedArray()) },
|
||||
)
|
||||
|
||||
FastScrollLazyColumn(contentPadding = contentPadding) {
|
||||
categoryMap.forEach { (category, items) ->
|
||||
downloadStatGroupUiItem(
|
||||
title = category,
|
||||
items = items,
|
||||
selectionMode = selectionMode,
|
||||
onCoverClick = onCoverClick,
|
||||
onSelected = onSelected,
|
||||
onMassSelected = onMassSelected,
|
||||
onDeleteManga = onDeleteManga,
|
||||
onGroupSelected = onGroupSelected,
|
||||
expanded = expanded,
|
||||
operations = operations,
|
||||
getGraphPoints = getGraphPoints,
|
||||
snackbarHostState = snackbarHostState,
|
||||
toggleExpanded = toggleExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.downloadStatGroupUiItem(
|
||||
items: List<DownloadStatManga>,
|
||||
selectionMode: Boolean,
|
||||
onSelected: (DownloadStatManga) -> Unit,
|
||||
onMassSelected: (DownloadStatManga) -> Unit,
|
||||
onCoverClick: (DownloadStatManga) -> Unit,
|
||||
title: String,
|
||||
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||
onGroupSelected: (List<DownloadStatManga>) -> Unit,
|
||||
toggleExpanded: (DownloadStatManga) -> Unit,
|
||||
expanded: MutableMap<String, Boolean>,
|
||||
operations: List<DownloadStatOperation>,
|
||||
getGraphPoints: (List<DownloadStatOperation>, Long, GraphGroupByMode) -> List<GraphicPoint>,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectedBackground(!items.fastAny { !it.selected })
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
expanded[title] = if (expanded[title] == null) {
|
||||
false
|
||||
} else {
|
||||
!expanded[title]!!
|
||||
}
|
||||
},
|
||||
onLongClick = { onGroupSelected(items) },
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = LocalContentColor.current.copy(alpha = 1f),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
Text(
|
||||
text = folderSizeText(items.sumOf { downloadStatManga -> downloadStatManga.folderSize }),
|
||||
)
|
||||
DotSeparatorText()
|
||||
Text(
|
||||
text = format(
|
||||
stringResource(R.string.group_info),
|
||||
items.sumOf { it.downloadChaptersCount },
|
||||
items.size,
|
||||
),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = LocalContentColor.current.copy(alpha = 1f),
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = if (expanded[title] == true) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (expanded[title] == true) {
|
||||
downloadStatUiItems(
|
||||
items = items,
|
||||
onCoverClick = onCoverClick,
|
||||
selectionMode = selectionMode,
|
||||
onDeleteManga = onDeleteManga,
|
||||
onSelected = onSelected,
|
||||
onMassSelected = onMassSelected,
|
||||
toggleExpanded = toggleExpanded,
|
||||
operations = operations,
|
||||
getGraphPoints = getGraphPoints,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.downloadStatUiItems(
|
||||
items: List<DownloadStatManga>,
|
||||
selectionMode: Boolean,
|
||||
onSelected: (DownloadStatManga) -> Unit,
|
||||
onMassSelected: (DownloadStatManga) -> Unit,
|
||||
onCoverClick: (DownloadStatManga) -> Unit,
|
||||
onDeleteManga: (List<DownloadStatManga>) -> Unit,
|
||||
toggleExpanded: (DownloadStatManga) -> Unit,
|
||||
operations: List<DownloadStatOperation>,
|
||||
getGraphPoints: (List<DownloadStatOperation>, Long, GraphGroupByMode) -> List<GraphicPoint>,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
items(
|
||||
items = items,
|
||||
) { item ->
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val noDataString = stringResource(R.string.no_stat_data)
|
||||
|
||||
val mangaOperations = operations.filter { it.mangaId == item.libraryManga.id }
|
||||
|
||||
DownloadStatUiItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
onLongClick = {
|
||||
if (selectionMode) {
|
||||
onMassSelected(item)
|
||||
} else {
|
||||
onSelected(item)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (selectionMode) {
|
||||
onSelected(item)
|
||||
} else if (mangaOperations.isNotEmpty()) {
|
||||
toggleExpanded(item)
|
||||
} else {
|
||||
scope.launch { snackbarHostState.showSnackbar(noDataString) }
|
||||
}
|
||||
},
|
||||
onCoverClick = { onCoverClick(item) },
|
||||
manga = item,
|
||||
onDeleteManga = { onDeleteManga(listOf(item)) }.takeIf { item.downloadChaptersCount > 0 },
|
||||
operations = mangaOperations,
|
||||
getGraphPoints = getGraphPoints,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadStatUiItem(
|
||||
modifier: Modifier,
|
||||
manga: DownloadStatManga,
|
||||
onCoverClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onDeleteManga: (() -> Unit)?,
|
||||
operations: List<DownloadStatOperation>,
|
||||
getGraphPoints: (List<DownloadStatOperation>, Long, GraphGroupByMode) -> List<GraphicPoint>,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.selectedBackground(manga.selected)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
MangaBaseInfoRow(modifier, manga, onCoverClick, onDeleteManga)
|
||||
if (manga.expanded) {
|
||||
MangaDownloadStatSection(operations)
|
||||
PointGraph(
|
||||
getPoints = { groupByMode ->
|
||||
getGraphPoints(
|
||||
operations,
|
||||
manga.folderSize,
|
||||
groupByMode,
|
||||
)
|
||||
},
|
||||
canvasHeight = 100f,
|
||||
supportLinesCount = 3,
|
||||
onColumnClick = null,
|
||||
showControls = false,
|
||||
showSubLine = false,
|
||||
columnWidth = 50f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaBaseInfoRow(
|
||||
modifier: Modifier,
|
||||
manga: DownloadStatManga,
|
||||
onCoverClick: () -> Unit,
|
||||
onDeleteManga: (() -> Unit)?,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MangaCover.Square(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.fillMaxHeight(),
|
||||
data = manga.libraryManga.manga,
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
|
||||
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 = 1f),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableIntStateOf(0) }
|
||||
Text(
|
||||
text = 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 = 1f),
|
||||
onTextLayout = { textHeight = it.size.height },
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f, fill = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onDeleteManga != null) {
|
||||
DownloadedIndicator(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
onClick = { onDeleteManga.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaDownloadStatSection(
|
||||
items: List<DownloadStatOperation>,
|
||||
) {
|
||||
Column {
|
||||
TitleRow(
|
||||
titles = listOf(
|
||||
stringResource(R.string.downloaded_chapters),
|
||||
items.filter { it.size > 0 }.sumOf { it.units }.toString(),
|
||||
folderSizeText(items.filter { it.size > 0 }.sumOf { it.size }),
|
||||
),
|
||||
titleStyle = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
TitleRow(
|
||||
titles = listOf(
|
||||
stringResource(R.string.deleted_chapters),
|
||||
items.filter { it.size < 0 }.sumOf { it.units }.toString(),
|
||||
folderSizeText(items.filter { it.size < 0 }.sumOf { abs(it.size) }),
|
||||
),
|
||||
titleStyle = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
SubTitleRow(
|
||||
subtitles = listOf(
|
||||
null,
|
||||
stringResource(R.string.label_total_chapters),
|
||||
stringResource(R.string.action_sort_size),
|
||||
),
|
||||
subtitleStyle = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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 folderSizeText(folderSizeBytes: Long): String {
|
||||
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())
|
||||
return if (exponent > 0) { String.format("%.2f %s", size, stringResource(units[exponent])) } else "0"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TitleRow(
|
||||
titles: List<String?> = emptyList(),
|
||||
titleStyle: TextStyle,
|
||||
) {
|
||||
Row {
|
||||
for (title in titles) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
text = title,
|
||||
style = titleStyle
|
||||
.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubTitleRow(
|
||||
subtitles: List<String?> = emptyList(),
|
||||
subtitleStyle: TextStyle,
|
||||
) {
|
||||
Row {
|
||||
for (subtitle in subtitles) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = subtitleStyle
|
||||
.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
.copy(alpha = SecondaryItemAlpha),
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package eu.kanade.presentation.more.download.components
|
||||
|
||||
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.res.stringResource
|
||||
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 eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
import kotlin.math.abs
|
||||
|
||||
@Composable
|
||||
fun DownloadStatOverviewSection(
|
||||
items: List<DownloadStatManga>,
|
||||
) {
|
||||
StatsSection(R.string.label_overview_section) {
|
||||
Row {
|
||||
StatsOverviewItem(
|
||||
title = folderSizeText(items.sumOf { it.folderSize }),
|
||||
subtitle = stringResource(R.string.action_sort_size),
|
||||
icon = Icons.Outlined.Storage,
|
||||
)
|
||||
StatsOverviewItem(
|
||||
title = items.sumOf { it.downloadChaptersCount }.toString(),
|
||||
subtitle = stringResource(R.string.chapters),
|
||||
icon = Icons.Outlined.Book,
|
||||
)
|
||||
StatsOverviewItem(
|
||||
title = items.size.toString(),
|
||||
subtitle = stringResource(R.string.manga),
|
||||
icon = Icons.Outlined.CollectionsBookmark,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadStatsRow(
|
||||
data: List<DownloadStatOperation>,
|
||||
) {
|
||||
StatsSection(R.string.downloaded_chapters) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.size.toString(),
|
||||
stringResource(R.string.label_total_chapters),
|
||||
)
|
||||
StatsItem(
|
||||
folderSizeText(data.sumOf { abs(it.size) }),
|
||||
stringResource(R.string.action_sort_size),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeletedStatsRow(
|
||||
data: List<DownloadStatOperation>,
|
||||
) {
|
||||
StatsSection(R.string.deleted_chapters) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.size.toString(),
|
||||
stringResource(R.string.label_total_chapters),
|
||||
)
|
||||
StatsItem(
|
||||
folderSizeText(abs(data.sumOf { it.size })),
|
||||
stringResource(R.string.action_sort_size),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
package eu.kanade.presentation.more.download.components.graphic
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowRightAlt
|
||||
import androidx.compose.material.icons.outlined.Cancel
|
||||
import androidx.compose.material.icons.outlined.DensitySmall
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.download.Dialog
|
||||
import eu.kanade.presentation.more.download.components.folderSizeText
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
import java.util.stream.IntStream.range
|
||||
|
||||
@Composable
|
||||
fun PointGraph(
|
||||
showSubLine: Boolean,
|
||||
getPoints: (GraphGroupByMode) -> List<GraphicPoint>,
|
||||
canvasHeight: Float,
|
||||
showControls: Boolean,
|
||||
supportLinesCount: Int,
|
||||
onColumnClick: ((Dialog?) -> Unit)?,
|
||||
columnWidth: Float = 80f,
|
||||
) {
|
||||
var groupByMode by remember { mutableStateOf(GraphGroupByMode.NONE) }
|
||||
val points = getPoints(groupByMode)
|
||||
val minValue = points.minOf { it.coordinate }
|
||||
val maxValue = points.maxOf { it.coordinate }
|
||||
val normalizedPoints = scaleData(
|
||||
data = points,
|
||||
targetMax = canvasHeight,
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
|
||||
if (normalizedPoints.size * columnWidth >= screenWidthDp.value) {
|
||||
Column {
|
||||
val graphScrollState = rememberLazyListState()
|
||||
if (showControls) {
|
||||
var groupByModeMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
GraphGroupDropdownMenu(
|
||||
expanded = groupByModeMenuExpanded,
|
||||
groupByMode = groupByMode,
|
||||
onGroupClicked = { graphGroupByMode -> groupByMode = graphGroupByMode },
|
||||
onDismissRequest = { groupByModeMenuExpanded = false },
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(30.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(
|
||||
enabled = graphScrollState.canScrollBackward,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
graphScrollState.scrollToItem(0)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowRightAlt,
|
||||
contentDescription = stringResource(R.string.scroll_to_start),
|
||||
modifier = Modifier
|
||||
.scale(scaleX = -1f, scaleY = 1f),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
graphScrollState.scrollToItem(normalizedPoints.size - 1)
|
||||
}
|
||||
},
|
||||
enabled = graphScrollState.canScrollForward,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowRightAlt,
|
||||
contentDescription = stringResource(R.string.scroll_to_end),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { groupByModeMenuExpanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DensitySmall,
|
||||
contentDescription = stringResource(R.string.action_group),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Row {
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(canvasHeight.dp),
|
||||
) {
|
||||
val spacing = 2f / (supportLinesCount - 1)
|
||||
val textUnderLineDashBase = 20
|
||||
val underLineDash = textUnderLineDashBase * 0.1f * (textUnderLineDashBase / canvasHeight)
|
||||
for (i in range(1, supportLinesCount - 1)) {
|
||||
Text(
|
||||
modifier = Modifier.align(
|
||||
BiasAlignment(
|
||||
1f,
|
||||
i * spacing - 1 - underLineDash * ((supportLinesCount - i).toFloat() / supportLinesCount),
|
||||
),
|
||||
),
|
||||
text = folderSizeText((minValue + (maxValue - minValue) * ((supportLinesCount - 1 - i).toFloat() / (supportLinesCount - 1))).toLong()),
|
||||
)
|
||||
}
|
||||
if (supportLinesCount >= 2) {
|
||||
Text(
|
||||
modifier = Modifier.align(BiasAlignment(1f, -1f - underLineDash)),
|
||||
text = folderSizeText(maxValue.toLong()),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.align(
|
||||
BiasAlignment(
|
||||
1f,
|
||||
1f,
|
||||
),
|
||||
),
|
||||
text = folderSizeText(minValue.toLong()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
LazyRow(
|
||||
state = graphScrollState,
|
||||
) {
|
||||
for (i in normalizedPoints.indices) {
|
||||
item {
|
||||
CenterPointItemWithSubAndLines(
|
||||
point = normalizedPoints[i],
|
||||
leftPoint = if (i > 0) normalizedPoints[i - 1].coordinate else null,
|
||||
rightPoint = if (i < normalizedPoints.size - 1) normalizedPoints[i + 1].coordinate else null,
|
||||
columnHeight = canvasHeight,
|
||||
linesCount = supportLinesCount,
|
||||
onClick = onColumnClick,
|
||||
showSubLine = showSubLine,
|
||||
columnWidth = columnWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (showControls) {
|
||||
Row {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height((canvasHeight + 50).dp),
|
||||
) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.no_enough_stat_data,
|
||||
actions = if (groupByMode != GraphGroupByMode.NONE) {
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.disable_stat_graph_grouping,
|
||||
icon = Icons.Outlined.Cancel,
|
||||
onClick = { groupByMode = GraphGroupByMode.NONE },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CenterPointItemWithSubAndLines(
|
||||
point: GraphicPoint,
|
||||
leftPoint: Float?,
|
||||
rightPoint: Float?,
|
||||
columnHeight: Float,
|
||||
linesCount: Int,
|
||||
onClick: ((Dialog?) -> Unit)?,
|
||||
showSubLine: Boolean,
|
||||
columnWidth: Float,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(columnWidth.dp)
|
||||
.clickable(
|
||||
onClick = {
|
||||
onClick?.invoke(point.dialog)
|
||||
},
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(columnHeight.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
val lineColor = LocalContentColor.current
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.height(columnHeight.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
val pointOffset = Offset(
|
||||
with(density) { (columnWidth / 2).dp.toPx() },
|
||||
with(density) { point.coordinate.dp.toPx() },
|
||||
)
|
||||
if (leftPoint != null) {
|
||||
drawLine(
|
||||
strokeWidth = 5f,
|
||||
color = lineColor,
|
||||
start = pointOffset,
|
||||
end = Offset(
|
||||
with(density) { ((columnWidth / 2) - columnWidth).dp.toPx() },
|
||||
with(density) { leftPoint.dp.toPx() },
|
||||
),
|
||||
)
|
||||
}
|
||||
if (rightPoint != null) {
|
||||
drawLine(
|
||||
strokeWidth = 5f,
|
||||
color = lineColor,
|
||||
start = pointOffset,
|
||||
end = Offset(
|
||||
with(density) { ((columnWidth / 2) + columnWidth).dp.toPx() },
|
||||
with(density) { rightPoint.dp.toPx() },
|
||||
),
|
||||
)
|
||||
}
|
||||
for (i in range(0, linesCount)) {
|
||||
drawSupportLine((columnHeight / (linesCount - 1)) * i, density)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showSubLine) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = point.subLine,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DrawScope.drawSupportLine(y: Float, density: Density) {
|
||||
drawLine(
|
||||
color = Color.Gray,
|
||||
start = Offset(
|
||||
with(density) { 0.dp.toPx() },
|
||||
with(density) { y.dp.toPx() },
|
||||
),
|
||||
end = Offset(
|
||||
with(density) { 80.dp.toPx() },
|
||||
with(density) { y.dp.toPx() },
|
||||
),
|
||||
alpha = 0.5F,
|
||||
strokeWidth = 5F,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GraphGroupDropdownMenu(
|
||||
expanded: Boolean,
|
||||
groupByMode: GraphGroupByMode,
|
||||
onDismissRequest: () -> Unit,
|
||||
onGroupClicked: (GraphGroupByMode) -> Unit,
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
listOfNotNull(
|
||||
if (groupByMode != GraphGroupByMode.NONE) GraphGroupByMode.NONE to stringResource(R.string.action_ungroup) else null,
|
||||
if (groupByMode != GraphGroupByMode.BY_DAY) GraphGroupByMode.BY_DAY to stringResource(R.string.action_group_by_day) else null,
|
||||
if (groupByMode != GraphGroupByMode.BY_MONTH) GraphGroupByMode.BY_MONTH to stringResource(R.string.action_group_by_month) else null,
|
||||
).map { (mode, string) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = string) },
|
||||
onClick = {
|
||||
onGroupClicked(mode)
|
||||
onDismissRequest()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun scaleData(data: List<GraphicPoint>, targetMax: Float): List<GraphicPoint> {
|
||||
val minValue = data.minOf { it.coordinate }
|
||||
val maxValue = data.maxOf { it.coordinate } - minValue
|
||||
return data
|
||||
.map {
|
||||
it.copy(
|
||||
coordinate = it.coordinate - minValue,
|
||||
)
|
||||
}
|
||||
.map {
|
||||
it.copy(
|
||||
coordinate = it.coordinate / (maxValue / targetMax),
|
||||
)
|
||||
}
|
||||
.map {
|
||||
it.copy(
|
||||
coordinate = (it.coordinate * -1) + targetMax,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class GraphicPoint(
|
||||
val coordinate: Float,
|
||||
val subLine: String,
|
||||
val dialog: Dialog? = null,
|
||||
)
|
||||
|
||||
enum class GraphGroupByMode {
|
||||
NONE,
|
||||
BY_DAY,
|
||||
BY_MONTH,
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
package eu.kanade.presentation.more.download.components.graphic
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.aspectRatio
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
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.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Swipe
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.download.GroupByMode
|
||||
import eu.kanade.presentation.more.download.data.DownloadStatManga
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.PieChartWithLegend(
|
||||
contentPadding: PaddingValues,
|
||||
getMap: (Boolean, GroupByMode) -> Map<String, List<DownloadStatManga>>,
|
||||
) {
|
||||
var groupByMode by remember { mutableStateOf(GroupByMode.BY_CATEGORY) }
|
||||
|
||||
val data = getMap(groupByMode == GroupByMode.BY_SOURCE, groupByMode)
|
||||
|
||||
val total = data.values.sumOf { list -> list.sumOf { it.folderSize } }
|
||||
|
||||
if (total > 0) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = contentPadding.calculateTopPadding(),
|
||||
start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
)
|
||||
.padding(
|
||||
horizontal = 10.dp,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(30.dp),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
when (groupByMode) {
|
||||
GroupByMode.BY_SOURCE -> groupByMode = GroupByMode.BY_CATEGORY
|
||||
GroupByMode.BY_CATEGORY -> groupByMode = GroupByMode.BY_SOURCE
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Swipe,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
val colors = generateColors(data.keys.toList())
|
||||
Row(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
) {
|
||||
PieChart(data = data, total = total, colors = colors)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
items(items = colors.entries.toList()) { entry ->
|
||||
LegendItem(
|
||||
name = entry.key,
|
||||
color = entry.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateColors(strings: List<String>): Map<String, Color> {
|
||||
val colorMap = mutableMapOf<String, Color>()
|
||||
val step = 360f / strings.size
|
||||
|
||||
for ((index, str) in strings.withIndex()) {
|
||||
val hue = (step * index) % 360
|
||||
val color = Color(android.graphics.Color.HSVToColor(floatArrayOf(hue, 1f, 1f)))
|
||||
colorMap[str] = color
|
||||
}
|
||||
|
||||
return colorMap
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PieChart(data: Map<String, List<DownloadStatManga>>, total: Long, colors: Map<String, Color>) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(100.dp, 500.dp)
|
||||
.aspectRatio(1f),
|
||||
onDraw = {
|
||||
val sorted = data.toList().sortedBy { pair -> pair.second.sumOf { it.folderSize } }
|
||||
var startAngle = 0f
|
||||
sorted.forEach { (name, value) ->
|
||||
val sweepAngle = (value.sumOf { it.folderSize }.toFloat() / total.toFloat()) * 360f
|
||||
drawArc(
|
||||
color = colors[name]!!,
|
||||
startAngle = startAngle,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = true,
|
||||
topLeft = Offset(0f, 0f),
|
||||
size = size,
|
||||
)
|
||||
startAngle += sweepAngle
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LegendItem(name: String, color: Color) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.background(color = color),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.presentation.more.download
|
||||
package eu.kanade.presentation.more.download.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
@ -8,9 +8,10 @@ import tachiyomi.domain.library.model.LibraryManga
|
||||
@Immutable
|
||||
data class DownloadStatManga(
|
||||
val libraryManga: LibraryManga,
|
||||
val folderSize: Long,
|
||||
val folderSize: Long = 0,
|
||||
val selected: Boolean = false,
|
||||
val expanded: Boolean = false,
|
||||
val source: Source,
|
||||
val category: Category,
|
||||
val downloadChaptersCount: Int,
|
||||
val downloadChaptersCount: Int = 0,
|
||||
)
|
@ -0,0 +1,169 @@
|
||||
package eu.kanade.presentation.more.download.tabs
|
||||
|
||||
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.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material.icons.outlined.Sort
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.presentation.more.download.DownloadStatsScreenModel
|
||||
import eu.kanade.presentation.more.download.DownloadStatsScreenState
|
||||
import eu.kanade.presentation.more.download.GroupByMode
|
||||
import eu.kanade.presentation.more.download.components.CategoryList
|
||||
import eu.kanade.presentation.more.download.components.downloadStatUiItems
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun downloadStatsItemsTab(
|
||||
state: DownloadStatsScreenState,
|
||||
screenModel: DownloadStatsScreenModel,
|
||||
): TabContent {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
|
||||
return TabContent(
|
||||
titleRes = R.string.label_download_enties_tab,
|
||||
searchEnabled = true,
|
||||
actions = listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_sort),
|
||||
icon = Icons.Outlined.Sort,
|
||||
onClick = { screenModel.openSettingsDialog() },
|
||||
),
|
||||
),
|
||||
actionModeActions = listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = { screenModel.toggleAllSelection(true) },
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = { screenModel.invertSelection() },
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.delete_downloads_for_manga),
|
||||
icon = Icons.Outlined.Delete,
|
||||
onClick = { screenModel.showDeleteAlert(state.selected) },
|
||||
),
|
||||
),
|
||||
content = { contentPadding, snackBarHost ->
|
||||
if (state.items.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(true),
|
||||
selectionMode = state.selectionMode,
|
||||
onCoverClick = { manga ->
|
||||
navigator.push(
|
||||
MangaScreen(manga.libraryManga.manga.id),
|
||||
)
|
||||
},
|
||||
onSelected = screenModel::toggleSelection,
|
||||
onMassSelected = screenModel::toggleMassSelection,
|
||||
onDeleteManga = screenModel::showDeleteAlert,
|
||||
operations = state.downloadStatOperations,
|
||||
getGraphPoints = { ops, weight, groupByMode ->
|
||||
screenModel.formGraphData(
|
||||
ops,
|
||||
weight,
|
||||
groupByMode,
|
||||
context,
|
||||
)
|
||||
},
|
||||
snackbarHostState = snackBarHost,
|
||||
toggleExpanded = screenModel::toggleExpanded,
|
||||
)
|
||||
}
|
||||
|
||||
GroupByMode.BY_SOURCE -> {
|
||||
CategoryList(
|
||||
contentPadding = contentPadding,
|
||||
selectionMode = state.selectionMode,
|
||||
onCoverClick = { item ->
|
||||
navigator.push(
|
||||
MangaScreen(item.libraryManga.manga.id),
|
||||
)
|
||||
},
|
||||
onDeleteManga = screenModel::showDeleteAlert,
|
||||
onGroupSelected = screenModel::groupSelection,
|
||||
onSelected = screenModel::toggleSelection,
|
||||
onMassSelected = screenModel::toggleMassSelection,
|
||||
categoryMap = screenModel.categoryMap(
|
||||
state.processedItems(true),
|
||||
GroupByMode.BY_SOURCE,
|
||||
state.sortMode,
|
||||
state.descendingOrder,
|
||||
defaultCategoryName = null,
|
||||
),
|
||||
operations = state.downloadStatOperations,
|
||||
getGraphPoints = { ops, weight, groupByMode ->
|
||||
screenModel.formGraphData(
|
||||
ops,
|
||||
weight,
|
||||
groupByMode,
|
||||
context,
|
||||
)
|
||||
},
|
||||
snackbarHostState = snackBarHost,
|
||||
toggleExpanded = screenModel::toggleExpanded,
|
||||
)
|
||||
}
|
||||
|
||||
GroupByMode.BY_CATEGORY -> {
|
||||
CategoryList(
|
||||
contentPadding = contentPadding,
|
||||
selectionMode = state.selectionMode,
|
||||
onCoverClick = { item ->
|
||||
navigator.push(
|
||||
MangaScreen(item.libraryManga.manga.id),
|
||||
)
|
||||
},
|
||||
onDeleteManga = screenModel::showDeleteAlert,
|
||||
onGroupSelected = screenModel::groupSelection,
|
||||
onSelected = screenModel::toggleSelection,
|
||||
onMassSelected = screenModel::toggleMassSelection,
|
||||
categoryMap = screenModel.categoryMap(
|
||||
items = state.processedItems(false),
|
||||
groupMode = GroupByMode.BY_CATEGORY,
|
||||
sortMode = state.sortMode,
|
||||
descendingOrder = state.descendingOrder,
|
||||
defaultCategoryName = stringResource(R.string.label_default),
|
||||
),
|
||||
operations = state.downloadStatOperations,
|
||||
getGraphPoints = { ops, weight, groupByMode ->
|
||||
screenModel.formGraphData(
|
||||
ops,
|
||||
weight,
|
||||
groupByMode,
|
||||
context,
|
||||
)
|
||||
},
|
||||
snackbarHostState = snackBarHost,
|
||||
toggleExpanded = screenModel::toggleExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package eu.kanade.presentation.more.download.tabs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.presentation.more.download.DownloadStatsScreenModel
|
||||
import eu.kanade.presentation.more.download.DownloadStatsScreenState
|
||||
import eu.kanade.presentation.more.download.SortingMode
|
||||
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.more.download.components.graphic.PieChartWithLegend
|
||||
import eu.kanade.presentation.more.download.components.graphic.PointGraph
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.presentation.core.components.LazyColumn
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun overAllStatsTab(
|
||||
state: DownloadStatsScreenState,
|
||||
screenModel: DownloadStatsScreenModel,
|
||||
): TabContent {
|
||||
return TabContent(
|
||||
titleRes = R.string.label_download_stats_overall_tab,
|
||||
searchEnabled = false,
|
||||
content = { PaddingValues, _ ->
|
||||
LazyColumn(
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues,
|
||||
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 })
|
||||
}
|
||||
item {
|
||||
val defaultCategoryName = stringResource(R.string.label_default)
|
||||
|
||||
PieChartWithLegend(
|
||||
getMap = {
|
||||
unique, groupByMode ->
|
||||
screenModel.categoryMap(
|
||||
items = if (unique) state.uniqueItems() else state.items,
|
||||
groupMode = groupByMode,
|
||||
sortMode = SortingMode.BY_SIZE,
|
||||
descendingOrder = true,
|
||||
defaultCategoryName = defaultCategoryName,
|
||||
)
|
||||
},
|
||||
contentPadding = PaddingValues,
|
||||
)
|
||||
}
|
||||
item {
|
||||
val context = LocalContext.current
|
||||
PointGraph(
|
||||
getPoints = { groupByMode ->
|
||||
screenModel.formGraphData(
|
||||
state.downloadStatOperations,
|
||||
state.items.sumOf { it.folderSize },
|
||||
groupByMode,
|
||||
context,
|
||||
)
|
||||
},
|
||||
canvasHeight = 200f,
|
||||
supportLinesCount = 5,
|
||||
onColumnClick = screenModel::setDialog,
|
||||
showSubLine = true,
|
||||
showControls = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -22,6 +22,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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@ -37,6 +39,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(),
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -223,6 +226,14 @@ class DownloadManager(
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val (mangaDir, chapterDirs) = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
addDownloadStatOperation.await(
|
||||
DownloadStatOperation.create().copy(
|
||||
mangaId = manga.id,
|
||||
size = chapterDirs.sumOf { provider.getFolderSize(it.filePath!!) } * -1,
|
||||
units = filteredChapters.size.toLong(),
|
||||
|
||||
),
|
||||
)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(filteredChapters, manga)
|
||||
|
||||
@ -245,7 +256,18 @@ class DownloadManager(
|
||||
if (removeQueued) {
|
||||
downloader.removeFromQueue(manga)
|
||||
}
|
||||
provider.findMangaDir(manga.title, source)?.delete()
|
||||
val mangaDir = provider.findMangaDir(manga.title, source)
|
||||
val dirSize = provider.getFolderSize(mangaDir?.filePath!!)
|
||||
if (dirSize > 0) {
|
||||
addDownloadStatOperation.await(
|
||||
DownloadStatOperation.create().copy(
|
||||
mangaId = manga.id,
|
||||
size = dirSize * -1,
|
||||
units = cache.getDownloadCount(manga).toLong(),
|
||||
),
|
||||
)
|
||||
}
|
||||
mangaDir.delete()
|
||||
cache.removeManga(manga)
|
||||
|
||||
// Delete source directory if empty
|
||||
|
@ -16,6 +16,7 @@ import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This class is used to provide the directories where the downloads should be saved.
|
||||
@ -188,4 +189,38 @@ class DownloadProvider(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return if (file.isDirectory) {
|
||||
getFolderSize(file.path)
|
||||
} else if (file.isFile) {
|
||||
file.length()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedOutputStream
|
||||
@ -74,6 +76,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(),
|
||||
) {
|
||||
@ -401,6 +404,13 @@ class Downloader(
|
||||
|
||||
DiskUtil.createNoMediaFile(tmpDir, context)
|
||||
|
||||
addDownloadStatOperation.await(
|
||||
DownloadStatOperation.create().copy(
|
||||
mangaId = download.manga.id,
|
||||
size = provider.getFolderSize(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
|
||||
|
14
data/src/main/java/tachiyomi/data/stat/DownloadStatMapper.kt
Normal file
14
data/src/main/java/tachiyomi/data/stat/DownloadStatMapper.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package tachiyomi.data.stat
|
||||
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
|
||||
val DownloadStatActionMapper: (Long, Long?, Long, Long, Long) ->
|
||||
DownloadStatOperation = { id, mangaId, date, size, units ->
|
||||
DownloadStatOperation(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
date = date * 1000,
|
||||
size = size,
|
||||
units = units,
|
||||
)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package tachiyomi.data.stat
|
||||
|
||||
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(DownloadStatActionMapper) }
|
||||
}
|
||||
|
||||
override suspend fun insert(operation: DownloadStatOperation) {
|
||||
handler.await {
|
||||
download_statQueries.insert(
|
||||
mangaId = operation.mangaId,
|
||||
date = operation.date / 1000,
|
||||
size = operation.size,
|
||||
units = operation.units,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
18
data/src/main/sqldelight/tachiyomi/data/download_stat.sq
Normal file
18
data/src/main/sqldelight/tachiyomi/data/download_stat.sq
Normal 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);
|
10
data/src/main/sqldelight/tachiyomi/migrations/27.sqm
Normal file
10
data/src/main/sqldelight/tachiyomi/migrations/27.sqm
Normal 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
|
||||
);
|
@ -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)
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package tachiyomi.domain.stat.interactor
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package tachiyomi.domain.stat.repository
|
||||
|
||||
import tachiyomi.domain.stat.model.DownloadStatOperation
|
||||
|
||||
interface DownloadStatRepository {
|
||||
|
||||
suspend fun getStatOperations(): List<DownloadStatOperation>
|
||||
|
||||
suspend fun insert(operation: DownloadStatOperation)
|
||||
}
|
@ -58,7 +58,6 @@
|
||||
<!-- reserved for #4048 -->
|
||||
<string name="action_filter_empty">Remove filter</string>
|
||||
<string name="action_sort_alpha">Alphabetically</string>
|
||||
<string name="action_sort_A_Z">A-Z</string>
|
||||
<string name="action_sort_count">Total entries</string>
|
||||
<string name="action_sort_total">Total chapters</string>
|
||||
<string name="action_sort_last_read">Last read</string>
|
||||
@ -112,7 +111,9 @@
|
||||
<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>
|
||||
<string name="action_ungroup">Ungroup</string>
|
||||
<string name="action_group_by_day">Day</string>
|
||||
<string name="action_group_by_month">Month</string>
|
||||
<!-- Do not translate "WebView" -->
|
||||
<string name="action_open_in_web_view">Open in WebView</string>
|
||||
<string name="action_web_view" translatable="false">WebView</string>
|
||||
@ -165,6 +166,23 @@
|
||||
<string name="action_faq_and_guides">FAQ and Guides</string>
|
||||
<string name="action_not_now">Not now</string>
|
||||
|
||||
<!-- downloads` stat`s screen -->
|
||||
<string name="label_download_enties_tab">Entries</string>
|
||||
<string name="label_download_stats_overall_tab">Overall</string>
|
||||
<string name="deleted_chapters">Deleted chapters</string>
|
||||
<string name="no_stat_data">No stat data</string>
|
||||
<string name="no_enough_stat_data">Not enough stat data to form graph</string>
|
||||
<string name="group_info">downloaded %d chapters in %d entries</string>
|
||||
<string name="download_stat_operation_deleted">Deleted %d chapter with weight %s</string>
|
||||
<string name="download_stat_operation_downloaded">Downloaded %d chapter with weight %s</string>
|
||||
<string name="entry_was_deleted">Entry was deleted from the library</string>
|
||||
<string name="graph_start_point_subLine">earlier</string>
|
||||
<string name="show_entries_without_downloaded">Show entries with no currently downloaded chapters</string>
|
||||
<string name="scroll_to_start">Scroll to start</string>
|
||||
<string name="scroll_to_end">Scroll to end</string>
|
||||
<string name="disable_stat_graph_grouping">Disable group mode</string>
|
||||
<string name="start_operation_massage">Start of recording, no information previously available</string>
|
||||
|
||||
<!-- Memory units -->
|
||||
<string name="memory_unit_b">B</string>
|
||||
<string name="memory_unit_kb">KB</string>
|
||||
@ -707,6 +725,7 @@
|
||||
<string name="error_saving_cover">Error saving cover</string>
|
||||
<string name="error_sharing_cover">Error sharing cover</string>
|
||||
<string name="confirm_delete_chapters">Are you sure you want to delete the selected chapters?</string>
|
||||
<string name="confirm_delete_entries">Are you sure you want to delete the selected entries?</string>
|
||||
<string name="chapter_settings">Chapter settings</string>
|
||||
<string name="confirm_set_chapter_settings">Are you sure you want to save these settings as default?</string>
|
||||
<string name="also_set_chapter_settings_for_library">Also apply to all entries in my library</string>
|
||||
|
Loading…
Reference in New Issue
Block a user