mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
feat: Add a storage viewer (#1188)
This commit is contained in:
parent
e728a60c68
commit
082d9e3395
22 changed files with 1031 additions and 25 deletions
|
@ -60,7 +60,10 @@ fun TriStateItem(
|
|||
},
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
|
@ -91,15 +94,18 @@ fun TriStateItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun SelectItem(
|
||||
fun <T> SelectItem(
|
||||
label: String,
|
||||
options: Array<out Any?>,
|
||||
options: Array<T>,
|
||||
selectedIndex: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
onSelect: (Int) -> Unit,
|
||||
toString: (T) -> String = { it.toString() },
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = modifier,
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
) {
|
||||
|
@ -107,9 +113,12 @@ fun SelectItem(
|
|||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
label = { Text(text = label) },
|
||||
value = options[selectedIndex].toString(),
|
||||
value = toString(options[selectedIndex]),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
|
@ -126,9 +135,9 @@ fun SelectItem(
|
|||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
options.forEachIndexed { index, text ->
|
||||
options.forEachIndexed { index, option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text.toString()) },
|
||||
text = { Text(toString(option)) },
|
||||
onClick = {
|
||||
onSelect(index)
|
||||
expanded = false
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.Label
|
|||
import androidx.compose.material.icons.outlined.QueryStats
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||
import androidx.compose.material.icons.outlined.Storage
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
@ -50,6 +51,7 @@ fun MoreScreen(
|
|||
onClickDownloadQueue: () -> Unit,
|
||||
onClickCategories: () -> Unit,
|
||||
onClickStats: () -> Unit,
|
||||
onClickStorage: () -> Unit,
|
||||
onClickBackupAndRestore: () -> Unit,
|
||||
onClickSettings: () -> Unit,
|
||||
onClickAbout: () -> Unit,
|
||||
|
@ -165,6 +167,13 @@ fun MoreScreen(
|
|||
onPreferenceClick = onClickStats,
|
||||
)
|
||||
}
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.label_storage),
|
||||
icon = Icons.Outlined.Storage,
|
||||
onPreferenceClick = onClickStorage,
|
||||
)
|
||||
}
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.label_backup),
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package eu.kanade.presentation.more.storage
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import eu.kanade.tachiyomi.util.toSize
|
||||
|
||||
@Composable
|
||||
fun CumulativeStorage(
|
||||
items: List<StorageItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
borderWidth: Float = 15f,
|
||||
) {
|
||||
val totalSize = remember(items) {
|
||||
items.sumOf { it.size }.toFloat()
|
||||
}
|
||||
val totalSizeString = remember(totalSize) {
|
||||
totalSize.toLong().toSize()
|
||||
}
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
Canvas(
|
||||
modifier = Modifier.aspectRatio(1f),
|
||||
onDraw = {
|
||||
val totalAngle = 180f
|
||||
var currentAngle = 0f
|
||||
rotate(180f) {
|
||||
for (item in items) {
|
||||
val itemAngle = (item.size / totalSize) * totalAngle
|
||||
drawArc(
|
||||
color = item.color,
|
||||
startAngle = currentAngle,
|
||||
sweepAngle = itemAngle,
|
||||
useCenter = false,
|
||||
style = Stroke(width = borderWidth, cap = StrokeCap.Round),
|
||||
)
|
||||
currentAngle += itemAngle
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = totalSizeString,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
)
|
||||
},
|
||||
measurePolicy = { measurables, constraints ->
|
||||
val placeables = measurables.map { measurable ->
|
||||
measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
|
||||
}
|
||||
val canvas = placeables.first()
|
||||
val text = placeables.last()
|
||||
// use half the height of the canvas to avoid too much extra space
|
||||
layout(constraints.maxWidth, canvas.height / 2) {
|
||||
canvas.placeRelative(0, 0)
|
||||
text.placeRelative(
|
||||
(canvas.width / 2) - (text.width / 2),
|
||||
(canvas.height / 4) - (text.height / 2),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.presentation.more.storage
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.SelectItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
||||
@Composable
|
||||
fun SelectStorageCategory(
|
||||
selectedCategory: Category,
|
||||
categories: List<Category>,
|
||||
modifier: Modifier = Modifier,
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
) {
|
||||
val all = stringResource(R.string.label_all)
|
||||
val default = stringResource(R.string.label_default)
|
||||
val mappedCategories = remember(categories) {
|
||||
categories.map {
|
||||
when (it.id) {
|
||||
-1L -> it.copy(name = all)
|
||||
Category.UNCATEGORIZED_ID -> it.copy(name = default)
|
||||
else -> it
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
SelectItem(
|
||||
modifier = modifier,
|
||||
label = stringResource(R.string.label_category),
|
||||
selectedIndex = mappedCategories.indexOfFirst { it.id == selectedCategory.id },
|
||||
options = mappedCategories,
|
||||
onSelect = { index ->
|
||||
onCategorySelected(mappedCategories[index])
|
||||
},
|
||||
toString = { it.name },
|
||||
)
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package eu.kanade.presentation.more.storage
|
||||
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.entries.ItemCover
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.toSize
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
data class StorageItem(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val size: Long,
|
||||
val thumbnail: String?,
|
||||
val entriesCount: Int,
|
||||
val color: Color,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun StorageItem(
|
||||
item: StorageItem,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onDelete: (Long) -> Unit,
|
||||
) {
|
||||
val pluralCount = if (isManga) R.plurals.manga_num_chapters else R.plurals.anime_num_episodes
|
||||
var showDeleteDialog by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = {
|
||||
ItemCover.Square(
|
||||
modifier = Modifier.height(48.dp),
|
||||
data = item.thumbnail,
|
||||
contentDescription = item.title,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
content = {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.W700,
|
||||
maxLines = 1,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(item.color, CircleShape)
|
||||
.size(12.dp),
|
||||
)
|
||||
Spacer(Modifier.width(MaterialTheme.padding.small))
|
||||
Text(
|
||||
text = item.size.toSize(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.small / 2)
|
||||
.background(MaterialTheme.colorScheme.onSurface, CircleShape)
|
||||
.size(MaterialTheme.padding.small / 2),
|
||||
)
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = pluralCount,
|
||||
count = item.entriesCount,
|
||||
item.entriesCount,
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
showDeleteDialog = true
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (showDeleteDialog) {
|
||||
ItemDeleteDialog(
|
||||
title = item.title,
|
||||
isManga = isManga,
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
onDelete = {
|
||||
onDelete(item.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemDeleteDialog(
|
||||
title: String,
|
||||
isManga: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
},
|
||||
content = {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
},
|
||||
)
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismissRequest,
|
||||
content = {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
},
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isManga) R.string.delete_downloads_for_manga else R.string.delete_downloads_for_anime,
|
||||
),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.delete_confirmation, title),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun StorageItemPreview() {
|
||||
StorageItem(
|
||||
item = StorageItem(
|
||||
id = 0L,
|
||||
title = "Manga Title",
|
||||
size = 123456789L,
|
||||
thumbnail = null,
|
||||
entriesCount = 123,
|
||||
color = Color.Red,
|
||||
),
|
||||
isManga = true,
|
||||
onDelete = {
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package eu.kanade.presentation.more.storage
|
||||
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.presentation.core.components.LazyColumn
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun StorageScreenContent(
|
||||
state: StorageScreenState,
|
||||
isManga: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues,
|
||||
onCategorySelected: (Category) -> Unit,
|
||||
onDelete: (Long) -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is StorageScreenState.Loading -> {
|
||||
LoadingScreen(modifier)
|
||||
}
|
||||
|
||||
is StorageScreenState.Success -> {
|
||||
@Composable
|
||||
fun Info(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
content = {
|
||||
SelectStorageCategory(
|
||||
selectedCategory = state.selectedCategory,
|
||||
categories = state.categories,
|
||||
onCategorySelected = onCategorySelected,
|
||||
)
|
||||
CumulativeStorage(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.small,
|
||||
vertical = MaterialTheme.padding.medium,
|
||||
)
|
||||
.run {
|
||||
if (isTabletUi()) {
|
||||
this
|
||||
} else {
|
||||
padding(bottom = MaterialTheme.padding.medium)
|
||||
}
|
||||
},
|
||||
items = state.items,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = MaterialTheme.padding.small)
|
||||
.padding(contentPadding),
|
||||
content = {
|
||||
if (isTabletUi()) {
|
||||
Info(
|
||||
modifier = Modifier
|
||||
.weight(2f)
|
||||
.padding(end = MaterialTheme.padding.extraLarge)
|
||||
.fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(3f),
|
||||
content = {
|
||||
item {
|
||||
Spacer(Modifier.height(MaterialTheme.padding.small))
|
||||
}
|
||||
item {
|
||||
if (!isTabletUi()) {
|
||||
Info()
|
||||
}
|
||||
}
|
||||
items(
|
||||
state.items.size,
|
||||
itemContent = { index ->
|
||||
StorageItem(
|
||||
item = state.items[index],
|
||||
isManga = isManga,
|
||||
onDelete = onDelete,
|
||||
)
|
||||
Spacer(Modifier.height(MaterialTheme.padding.medium))
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun StorageScreenContentPreview() {
|
||||
val random = remember { Random(0) }
|
||||
val categories = remember {
|
||||
List(10) {
|
||||
Category(
|
||||
id = it.toLong(),
|
||||
name = "Category $it",
|
||||
0L,
|
||||
0L,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
StorageScreenContent(
|
||||
state = StorageScreenState.Success(
|
||||
items = List(20) { index ->
|
||||
StorageItem(
|
||||
id = index.toLong(),
|
||||
title = "Title $index",
|
||||
size = index * 10000000L,
|
||||
thumbnail = null,
|
||||
entriesCount = 100 * index,
|
||||
color = Color(
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
),
|
||||
)
|
||||
},
|
||||
categories = categories,
|
||||
selectedCategory = categories[0],
|
||||
),
|
||||
isManga = true,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
onCategorySelected = {},
|
||||
onDelete = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.DESKTOP)
|
||||
@Composable
|
||||
private fun StorageTabletUiScreenContentPreview() {
|
||||
val random = remember { Random(0) }
|
||||
val categories = remember {
|
||||
List(10) {
|
||||
Category(
|
||||
id = it.toLong(),
|
||||
name = "Category $it",
|
||||
0L,
|
||||
0L,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
StorageScreenContent(
|
||||
state = StorageScreenState.Success(
|
||||
items = List(20) { index ->
|
||||
StorageItem(
|
||||
id = index.toLong(),
|
||||
title = "Title $index",
|
||||
size = index * 10000000L,
|
||||
thumbnail = null,
|
||||
entriesCount = 100 * index,
|
||||
color = Color(
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
),
|
||||
)
|
||||
},
|
||||
categories = categories,
|
||||
selectedCategory = categories[0],
|
||||
),
|
||||
isManga = true,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
onCategorySelected = {},
|
||||
onDelete = {},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.presentation.more.storage
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
||||
sealed class StorageScreenState {
|
||||
@Immutable
|
||||
object Loading : StorageScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val selectedCategory: Category,
|
||||
val items: List<StorageItem>,
|
||||
val categories: List<Category>,
|
||||
) : StorageScreenState()
|
||||
}
|
|
@ -6,6 +6,7 @@ import com.hippo.unifile.UniFile
|
|||
import eu.kanade.core.util.mapNotNullKeys
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.util.size
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -150,6 +151,32 @@ class AnimeDownloadCache(
|
|||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total size of downloaded episodes.
|
||||
*/
|
||||
fun getTotalDownloadSize(): Long {
|
||||
renewCache()
|
||||
|
||||
return rootDownloadsDir.sourceDirs.values.sumOf { sourceDir ->
|
||||
sourceDir.dir.size()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total size of downloaded chapters for an anime.
|
||||
*
|
||||
* @param anime the anime to check.
|
||||
*/
|
||||
fun getDownloadSize(anime: Anime): Long {
|
||||
renewCache()
|
||||
|
||||
return rootDownloadsDir.sourceDirs[anime.source]?.animeDirs?.get(
|
||||
provider.getAnimeDirName(
|
||||
anime.title,
|
||||
),
|
||||
)?.dir?.size() ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an episode that has just been download to this cache.
|
||||
*
|
||||
|
|
|
@ -128,7 +128,13 @@ class AnimeDownloadManager(
|
|||
* @param autoStart whether to start the downloader after enqueuing the episodes.
|
||||
* @param alt whether to use the alternative downloader
|
||||
*/
|
||||
fun downloadEpisodes(anime: Anime, episodes: List<Episode>, autoStart: Boolean = true, alt: Boolean = false, video: Video? = null) {
|
||||
fun downloadEpisodes(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
autoStart: Boolean = true,
|
||||
alt: Boolean = false,
|
||||
video: Video? = null,
|
||||
) {
|
||||
downloader.queueEpisodes(anime, episodes, autoStart, alt, video)
|
||||
}
|
||||
|
||||
|
@ -155,7 +161,8 @@ class AnimeDownloadManager(
|
|||
* @return an observable containing the list of pages from the episode.
|
||||
*/
|
||||
fun buildVideo(source: AnimeSource, anime: Anime, episode: Episode): Observable<Video> {
|
||||
val episodeDir = provider.findEpisodeDir(episode.name, episode.scanlator, anime.title, source)
|
||||
val episodeDir =
|
||||
provider.findEpisodeDir(episode.name, episode.scanlator, anime.title, source)
|
||||
return Observable.fromCallable {
|
||||
val files = episodeDir?.listFiles().orEmpty()
|
||||
.filter { "video" in it.type.orEmpty() }
|
||||
|
@ -165,7 +172,12 @@ class AnimeDownloadManager(
|
|||
}
|
||||
|
||||
val file = files[0]
|
||||
Video(file.uri.toString(), "download: " + file.uri.toString(), file.uri.toString(), file.uri).apply { status = Video.State.READY }
|
||||
Video(
|
||||
file.uri.toString(),
|
||||
"download: " + file.uri.toString(),
|
||||
file.uri.toString(),
|
||||
file.uri,
|
||||
).apply { status = Video.State.READY }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,7 +197,13 @@ class AnimeDownloadManager(
|
|||
sourceId: Long,
|
||||
skipCache: Boolean = false,
|
||||
): Boolean {
|
||||
return cache.isEpisodeDownloaded(episodeName, episodeScanlator, animeTitle, sourceId, skipCache)
|
||||
return cache.isEpisodeDownloaded(
|
||||
episodeName,
|
||||
episodeScanlator,
|
||||
animeTitle,
|
||||
sourceId,
|
||||
skipCache,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,6 +222,22 @@ class AnimeDownloadManager(
|
|||
return cache.getDownloadCount(anime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of downloaded episodes.
|
||||
*/
|
||||
fun getDownloadSize(): Long {
|
||||
return cache.getTotalDownloadSize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of downloaded episodes for an anime.
|
||||
*
|
||||
* @param anime the anime to check.
|
||||
*/
|
||||
fun getDownloadSize(anime: Anime): Long {
|
||||
return cache.getDownloadSize(anime)
|
||||
}
|
||||
|
||||
fun cancelQueuedDownloads(downloads: List<AnimeDownload>) {
|
||||
removeFromDownloadQueue(downloads.map { it.episode })
|
||||
}
|
||||
|
@ -221,7 +255,11 @@ class AnimeDownloadManager(
|
|||
launchIO {
|
||||
removeFromDownloadQueue(filteredEpisodes)
|
||||
|
||||
val (animeDir, episodeDirs) = provider.findEpisodeDirs(filteredEpisodes, anime, source)
|
||||
val (animeDir, episodeDirs) = provider.findEpisodeDirs(
|
||||
filteredEpisodes,
|
||||
anime,
|
||||
source,
|
||||
)
|
||||
episodeDirs.forEach { it.delete() }
|
||||
cache.removeEpisodes(filteredEpisodes, anime)
|
||||
|
||||
|
@ -346,7 +384,8 @@ class AnimeDownloadManager(
|
|||
|
||||
private fun getEpisodesToDelete(episodes: List<Episode>, anime: Anime): List<Episode> {
|
||||
// Retrieve the categories that are set to exclude from being deleted on read
|
||||
val categoriesToExclude = downloadPreferences.removeExcludeAnimeCategories().get().map(String::toLong)
|
||||
val categoriesToExclude =
|
||||
downloadPreferences.removeExcludeAnimeCategories().get().map(String::toLong)
|
||||
|
||||
val categoriesForAnime = runBlocking { getCategories.await(anime.id) }
|
||||
.map { it.id }
|
||||
|
@ -372,7 +411,8 @@ class AnimeDownloadManager(
|
|||
}
|
||||
.onStart {
|
||||
emitAll(
|
||||
queueState.value.filter { download -> download.status == AnimeDownload.State.DOWNLOADING }.asFlow(),
|
||||
queueState.value.filter { download -> download.status == AnimeDownload.State.DOWNLOADING }
|
||||
.asFlow(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.hippo.unifile.UniFile
|
|||
import eu.kanade.core.util.mapNotNullKeys
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.util.size
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -141,7 +142,12 @@ class MangaDownloadCache(
|
|||
): Boolean {
|
||||
if (skipCache) {
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
|
||||
return provider.findChapterDir(
|
||||
chapterName,
|
||||
chapterScanlator,
|
||||
mangaTitle,
|
||||
source,
|
||||
) != null
|
||||
}
|
||||
|
||||
renewCache()
|
||||
|
@ -150,7 +156,8 @@ class MangaDownloadCache(
|
|||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(mangaTitle)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getValidChapterDirNames(chapterName, chapterScanlator).any { it in mangaDir.chapterDirs }
|
||||
return provider.getValidChapterDirNames(chapterName, chapterScanlator)
|
||||
.any { it in mangaDir.chapterDirs }
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -187,6 +194,32 @@ class MangaDownloadCache(
|
|||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total size of downloaded chapters.
|
||||
*/
|
||||
fun getTotalDownloadSize(): Long {
|
||||
renewCache()
|
||||
|
||||
return rootDownloadsDir.sourceDirs.values.sumOf { sourceDir ->
|
||||
sourceDir.dir.size()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total size of downloaded chapters for a manga.
|
||||
*
|
||||
* @param manga the manga to check.
|
||||
*/
|
||||
fun getDownloadSize(manga: Manga): Long {
|
||||
renewCache()
|
||||
|
||||
return rootDownloadsDir.sourceDirs[manga.source]?.mangaDirs?.get(
|
||||
provider.getMangaDirName(
|
||||
manga.title,
|
||||
),
|
||||
)?.dir?.size() ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a chapter that has just been download to this cache.
|
||||
*
|
||||
|
@ -452,11 +485,13 @@ private class MangaDirectory(
|
|||
)
|
||||
|
||||
private object UniFileAsStringSerializer : KSerializer<UniFile> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UniFile) {
|
||||
return encoder.encodeString(value.uri.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UniFile {
|
||||
return UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
||||
}
|
||||
|
|
|
@ -201,6 +201,22 @@ class MangaDownloadManager(
|
|||
return cache.getDownloadCount(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of downloaded chapters.
|
||||
*/
|
||||
fun getDownloadSize(): Long {
|
||||
return cache.getTotalDownloadSize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of downloaded chapters for a manga.
|
||||
*
|
||||
* @param manga the manga to check.
|
||||
*/
|
||||
fun getDownloadSize(manga: Manga): Long {
|
||||
return cache.getDownloadSize(manga)
|
||||
}
|
||||
|
||||
fun cancelQueuedDownloads(downloads: List<MangaDownload>) {
|
||||
removeFromDownloadQueue(downloads.map { it.chapter })
|
||||
}
|
||||
|
|
|
@ -121,10 +121,11 @@ private fun FilterItem(filter: AnimeFilter<*>, onUpdate: () -> Unit) {
|
|||
label = filter.name,
|
||||
options = filter.values,
|
||||
selectedIndex = filter.state,
|
||||
) {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
}
|
||||
onSelect = {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
},
|
||||
)
|
||||
}
|
||||
is AnimeFilter.Sort -> {
|
||||
CollapsibleBox(
|
||||
|
|
|
@ -121,10 +121,11 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) {
|
|||
label = filter.name,
|
||||
options = filter.values,
|
||||
selectedIndex = filter.state,
|
||||
) {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
}
|
||||
onSelect = {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
},
|
||||
)
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
CollapsibleBox(
|
||||
|
|
|
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.history.HistoriesTab
|
|||
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryTab
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
||||
import eu.kanade.tachiyomi.ui.stats.StatsTab
|
||||
import eu.kanade.tachiyomi.ui.storage.StorageTab
|
||||
import eu.kanade.tachiyomi.ui.updates.UpdatesTab
|
||||
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -78,6 +79,7 @@ object MoreTab : Tab() {
|
|||
onClickDownloadQueue = { navigator.push(DownloadsTab()) },
|
||||
onClickCategories = { navigator.push(CategoriesTab()) },
|
||||
onClickStats = { navigator.push(StatsTab()) },
|
||||
onClickStorage = { navigator.push(StorageTab()) },
|
||||
onClickBackupAndRestore = { navigator.push(SettingsScreen.toBackupScreen()) },
|
||||
onClickSettings = { navigator.push(SettingsScreen.toMainScreen()) },
|
||||
onClickAbout = { navigator.push(SettingsScreen.toAboutScreen()) },
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package eu.kanade.tachiyomi.ui.storage
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.presentation.more.storage.StorageItem
|
||||
import eu.kanade.presentation.more.storage.StorageScreenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import kotlin.random.Random
|
||||
|
||||
abstract class CommonStorageScreenModel<T>(
|
||||
private val downloadCacheChanges: SharedFlow<Unit>,
|
||||
private val downloadCacheIsInitializing: StateFlow<Boolean>,
|
||||
private val libraries: Flow<List<T>>,
|
||||
private val categories: Flow<List<Category>>,
|
||||
private val getTotalDownloadSize: () -> Long,
|
||||
private val getDownloadSize: T.() -> Long,
|
||||
private val getDownloadCount: T.() -> Int,
|
||||
private val getId: T.() -> Long,
|
||||
private val getCategoryId: T.() -> Long,
|
||||
private val getTitle: T.() -> String,
|
||||
private val getThumbnail: T.() -> String?,
|
||||
) : StateScreenModel<StorageScreenState>(StorageScreenState.Loading) {
|
||||
|
||||
private val selectedCategory = MutableStateFlow(AllCategory)
|
||||
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
combine(
|
||||
flow = downloadCacheChanges,
|
||||
flow2 = downloadCacheIsInitializing,
|
||||
flow3 = libraries,
|
||||
flow4 = categories,
|
||||
flow5 = selectedCategory,
|
||||
transform = { _, _, libraries, categories, selectedCategory ->
|
||||
val distinctLibraries = libraries.fastDistinctBy {
|
||||
it.getId()
|
||||
}.filter {
|
||||
selectedCategory == AllCategory || it.getCategoryId() == selectedCategory.id
|
||||
}
|
||||
val size = getTotalDownloadSize()
|
||||
val random = Random(size + distinctLibraries.size)
|
||||
|
||||
mutableState.update {
|
||||
StorageScreenState.Success(
|
||||
selectedCategory = selectedCategory,
|
||||
categories = listOf(AllCategory, *categories.toTypedArray()),
|
||||
items = distinctLibraries.map {
|
||||
StorageItem(
|
||||
id = it.getId(),
|
||||
title = it.getTitle(),
|
||||
size = it.getDownloadSize(),
|
||||
thumbnail = it.getThumbnail(),
|
||||
entriesCount = it.getDownloadCount(),
|
||||
color = Color(
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
random.nextInt(255),
|
||||
),
|
||||
)
|
||||
}.sortedByDescending { it.size },
|
||||
)
|
||||
}
|
||||
},
|
||||
).collectLatest {}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedCategory(category: Category) {
|
||||
selectedCategory.update { category }
|
||||
}
|
||||
|
||||
abstract fun deleteEntry(id: Long)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A dummy category used to display all entries irrespective of the category.
|
||||
*/
|
||||
private val AllCategory = Category(
|
||||
id = -1L,
|
||||
name = "All",
|
||||
order = 0L,
|
||||
flags = 0L,
|
||||
hidden = false,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package eu.kanade.tachiyomi.ui.storage
|
||||
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.storage.anime.animeStorageTab
|
||||
import eu.kanade.tachiyomi.ui.storage.manga.mangaStorageTab
|
||||
|
||||
data class StorageTab(
|
||||
private val isManga: Boolean = false,
|
||||
) : Tab() {
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable
|
||||
get() {
|
||||
val isSelected = LocalTabNavigator.current.current.key == key
|
||||
val image = AnimatedImageVector.animatedVectorResource(R.drawable.anim_updates_enter)
|
||||
return TabOptions(
|
||||
index = 8u,
|
||||
title = stringResource(R.string.label_storage),
|
||||
icon = rememberAnimatedVectorPainter(image, isSelected),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
|
||||
TabbedScreen(
|
||||
titleRes = R.string.label_storage,
|
||||
tabs = listOf(
|
||||
animeStorageTab(),
|
||||
mangaStorageTab(),
|
||||
),
|
||||
startIndex = 1.takeIf { isManga },
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
(context as? MainActivity)?.ready = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package eu.kanade.tachiyomi.ui.storage.anime
|
||||
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
|
||||
import eu.kanade.tachiyomi.ui.storage.CommonStorageScreenModel
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.domain.category.anime.interactor.GetVisibleAnimeCategories
|
||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||
import tachiyomi.domain.library.anime.LibraryAnime
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AnimeStorageScreenModel(
|
||||
downloadCache: AnimeDownloadCache = Injekt.get(),
|
||||
private val getLibraries: GetLibraryAnime = Injekt.get(),
|
||||
getVisibleCategories: GetVisibleAnimeCategories = Injekt.get(),
|
||||
private val downloadManager: AnimeDownloadManager = Injekt.get(),
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
) : CommonStorageScreenModel<LibraryAnime>(
|
||||
downloadCacheChanges = downloadCache.changes,
|
||||
downloadCacheIsInitializing = downloadCache.isInitializing,
|
||||
libraries = getLibraries.subscribe(),
|
||||
categories = getVisibleCategories.subscribe(),
|
||||
getTotalDownloadSize = { downloadManager.getDownloadSize() },
|
||||
getDownloadSize = { downloadManager.getDownloadSize(anime) },
|
||||
getDownloadCount = { downloadManager.getDownloadCount(anime) },
|
||||
getId = { id },
|
||||
getCategoryId = { category },
|
||||
getTitle = { anime.title },
|
||||
getThumbnail = { anime.thumbnailUrl },
|
||||
) {
|
||||
override fun deleteEntry(id: Long) {
|
||||
coroutineScope.launchNonCancellable {
|
||||
val anime = getLibraries.await().find {
|
||||
it.id == id
|
||||
}?.anime ?: return@launchNonCancellable
|
||||
val source = sourceManager.get(anime.source) ?: return@launchNonCancellable
|
||||
downloadManager.deleteAnime(anime, source)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package eu.kanade.tachiyomi.ui.storage.anime
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.presentation.more.storage.StorageScreenContent
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun Screen.animeStorageTab(): TabContent {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { AnimeStorageScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
return TabContent(
|
||||
titleRes = R.string.label_anime,
|
||||
content = { contentPadding, _ ->
|
||||
StorageScreenContent(
|
||||
state = state,
|
||||
isManga = false,
|
||||
contentPadding = contentPadding,
|
||||
onCategorySelected = screenModel::setSelectedCategory,
|
||||
onDelete = screenModel::deleteEntry,
|
||||
)
|
||||
},
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package eu.kanade.tachiyomi.ui.storage.manga
|
||||
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||
import eu.kanade.tachiyomi.ui.storage.CommonStorageScreenModel
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.domain.category.manga.interactor.GetVisibleMangaCategories
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.library.manga.LibraryManga
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaStorageScreenModel(
|
||||
downloadCache: MangaDownloadCache = Injekt.get(),
|
||||
private val getLibraries: GetLibraryManga = Injekt.get(),
|
||||
getVisibleCategories: GetVisibleMangaCategories = Injekt.get(),
|
||||
private val downloadManager: MangaDownloadManager = Injekt.get(),
|
||||
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||
) : CommonStorageScreenModel<LibraryManga>(
|
||||
downloadCacheChanges = downloadCache.changes,
|
||||
downloadCacheIsInitializing = downloadCache.isInitializing,
|
||||
libraries = getLibraries.subscribe(),
|
||||
categories = getVisibleCategories.subscribe(),
|
||||
getTotalDownloadSize = { downloadManager.getDownloadSize() },
|
||||
getDownloadSize = { downloadManager.getDownloadSize(manga) },
|
||||
getDownloadCount = { downloadManager.getDownloadCount(manga) },
|
||||
getId = { id },
|
||||
getCategoryId = { category },
|
||||
getTitle = { manga.title },
|
||||
getThumbnail = { manga.thumbnailUrl },
|
||||
) {
|
||||
override fun deleteEntry(id: Long) {
|
||||
coroutineScope.launchNonCancellable {
|
||||
val manga = getLibraries.await().find {
|
||||
it.id == id
|
||||
}?.manga ?: return@launchNonCancellable
|
||||
val source = sourceManager.get(manga.source) ?: return@launchNonCancellable
|
||||
downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package eu.kanade.tachiyomi.ui.storage.manga
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.TabContent
|
||||
import eu.kanade.presentation.more.storage.StorageScreenContent
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun Screen.mangaStorageTab(): TabContent {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { MangaStorageScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
return TabContent(
|
||||
titleRes = R.string.label_manga,
|
||||
content = { contentPadding, _ ->
|
||||
StorageScreenContent(
|
||||
state = state,
|
||||
isManga = true,
|
||||
contentPadding = contentPadding,
|
||||
onCategorySelected = screenModel::setSelectedCategory,
|
||||
onDelete = screenModel::deleteEntry,
|
||||
)
|
||||
},
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
}
|
38
app/src/main/java/eu/kanade/tachiyomi/util/StorageUtil.kt
Normal file
38
app/src/main/java/eu/kanade/tachiyomi/util/StorageUtil.kt
Normal file
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
|
||||
/**
|
||||
* Converts a long to a readable file size.
|
||||
*/
|
||||
fun Long.toSize(): String {
|
||||
val kb = 1000
|
||||
val mb = kb * kb
|
||||
val gb = mb * kb
|
||||
return when {
|
||||
this >= gb -> "%.2f GB".format(this.toFloat() / gb)
|
||||
this >= mb -> "%.2f MB".format(this.toFloat() / mb)
|
||||
this >= kb -> "%.2f KB".format(this.toFloat() / kb)
|
||||
else -> "$this B"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of a file or directory.
|
||||
*/
|
||||
fun UniFile.size(): Long {
|
||||
var totalSize = 0L
|
||||
if (isDirectory) {
|
||||
listFiles()?.forEach { file ->
|
||||
totalSize += if (file.isDirectory) {
|
||||
file.size()
|
||||
} else {
|
||||
val length = file.length()
|
||||
if (length > 0) length else 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalSize = length()
|
||||
}
|
||||
return totalSize
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
<!-- Actions -->
|
||||
<string name="action_hide">Hide</string>
|
||||
|
||||
<string name="label_all">All</string>
|
||||
<string name="label_category">Category</string>
|
||||
<string name="manga_categories">Manga Categories</string>
|
||||
<string name="general_categories">Categories</string>
|
||||
<string name="anime_categories">Anime Categories</string>
|
||||
|
@ -177,6 +179,7 @@
|
|||
<item quantity="one">%1$s episode</item>
|
||||
<item quantity="other">%1$s episodes</item>
|
||||
</plurals>
|
||||
<string name="delete_confirmation">Are you sure you wish to delete \"%s\"?</string>
|
||||
<string name="delete_downloads_for_anime">Delete downloaded episodes?</string>
|
||||
<string name="snack_add_to_manga_library">Add manga to library?</string>
|
||||
<string name="snack_add_to_anime_library">Add anime to library?</string>
|
||||
|
@ -250,6 +253,7 @@
|
|||
<item quantity="other">%d seconds</item>
|
||||
</plurals>
|
||||
<string name="no_next_episode">Next Episode not found!</string>
|
||||
<string name="label_storage">Storage</string>
|
||||
<string name="label_history">Manga</string>
|
||||
<string name="label_anime_history">Anime</string>
|
||||
<string name="label_updates">Manga</string>
|
||||
|
|
Loading…
Reference in a new issue