Storage screen
This commit is contained in:
@@ -84,8 +84,8 @@ dependencies {
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.6.0")
|
||||
implementation("androidx.compose.material3:material3-android:1.2.0-rc01")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
|
||||
implementation("androidx.compose.material3:material3-android:1.2.0")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.2.0")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
||||
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||
@@ -94,6 +94,7 @@ dependencies {
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
ksp("androidx.room:room-compiler:2.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("com.google.accompanist:accompanist-flowlayout:0.32.0")
|
||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
|
||||
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
||||
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
|
||||
|
||||
@@ -45,6 +45,8 @@ import org.xtimms.tokusho.sections.settings.appearance.DARK_THEME_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.appearance.DarkThemeView
|
||||
import org.xtimms.tokusho.sections.settings.appearance.LANGUAGES_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.appearance.LanguagesView
|
||||
import org.xtimms.tokusho.sections.settings.storage.STORAGE_DESTINATION
|
||||
import org.xtimms.tokusho.sections.settings.storage.StorageView
|
||||
import org.xtimms.tokusho.sections.shelf.ShelfMap
|
||||
import org.xtimms.tokusho.sections.shelf.ShelfView
|
||||
import org.xtimms.tokusho.utils.lang.removeFirstAndLast
|
||||
@@ -144,7 +146,8 @@ fun Navigation(
|
||||
navigateBack = navigateBack,
|
||||
navigateToAppearance = { navController.navigate(APPEARANCE_DESTINATION) },
|
||||
navigateToAbout = { navController.navigate(ABOUT_DESTINATION) },
|
||||
navigateToAdvanced = { navController.navigate(ADVANCED_DESTINATION) }
|
||||
navigateToAdvanced = { navController.navigate(ADVANCED_DESTINATION) },
|
||||
navigateToStorage = { navController.navigate(STORAGE_DESTINATION) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,6 +172,12 @@ fun Navigation(
|
||||
)
|
||||
}
|
||||
|
||||
composable(STORAGE_DESTINATION) {
|
||||
StorageView(
|
||||
navigateBack = navigateBack,
|
||||
)
|
||||
}
|
||||
|
||||
composable(ADVANCED_DESTINATION) {
|
||||
AdvancedView(
|
||||
navigateBack = navigateBack,
|
||||
|
||||
@@ -9,8 +9,28 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.xtimms.tokusho.R
|
||||
|
||||
@Composable
|
||||
fun ConfirmButton(
|
||||
text: String = stringResource(R.string.confirm),
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextButton(onClick = onClick, enabled = enabled) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DismissButton(text: String = stringResource(R.string.dismiss), onClick: () -> Unit) {
|
||||
TextButton(onClick = onClick) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.xtimms.tokusho.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun DialogCheckBoxItem(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = checked,
|
||||
enabled = true,
|
||||
onValueChange = { onClick() },
|
||||
)
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
checked = checked, onCheckedChange = { onClick() },
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
137
app/src/main/java/org/xtimms/tokusho/core/components/Dialogs.kt
Normal file
137
app/src/main/java/org/xtimms/tokusho/core/components/Dialogs.kt
Normal file
@@ -0,0 +1,137 @@
|
||||
package org.xtimms.tokusho.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
private val DialogVerticalPadding = PaddingValues(vertical = 24.dp)
|
||||
private val IconPadding = PaddingValues(bottom = 16.dp)
|
||||
private val DialogHorizontalPadding = PaddingValues(horizontal = 24.dp)
|
||||
private val TitlePadding = PaddingValues(bottom = 16.dp)
|
||||
private val TextPadding = PaddingValues(bottom = 24.dp)
|
||||
private val ButtonsMainAxisSpacing = Arrangement.spacedBy(8.dp, Alignment.Start)
|
||||
private val ButtonsCrossAxisSpacing = Arrangement.spacedBy(12.dp, Alignment.Top)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun TokushoDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
confirmButton: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
dismissButton: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = AlertDialogDefaults.shape,
|
||||
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||
properties: DialogProperties = DialogProperties()
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
properties = properties
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
tonalElevation = tonalElevation,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(DialogVerticalPadding)
|
||||
) {
|
||||
icon?.let {
|
||||
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(IconPadding)
|
||||
.padding(DialogHorizontalPadding)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
}
|
||||
title?.let {
|
||||
CompositionLocalProvider(LocalContentColor provides titleContentColor) {
|
||||
val textStyle = MaterialTheme.typography.headlineSmall
|
||||
ProvideTextStyle(textStyle) {
|
||||
Box(
|
||||
// Align the title to the center when an icon is present.
|
||||
Modifier
|
||||
.padding(TitlePadding)
|
||||
.padding(DialogHorizontalPadding)
|
||||
.align(
|
||||
if (icon == null) {
|
||||
Alignment.Start
|
||||
} else {
|
||||
Alignment.CenterHorizontally
|
||||
}
|
||||
)
|
||||
) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text?.let {
|
||||
CompositionLocalProvider(LocalContentColor provides textContentColor) {
|
||||
val textStyle =
|
||||
MaterialTheme.typography.bodyMedium
|
||||
ProvideTextStyle(textStyle) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.padding(TextPadding)
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(DialogHorizontalPadding)
|
||||
) {
|
||||
val textStyle =
|
||||
MaterialTheme.typography.labelLarge
|
||||
ProvideTextStyle(value = textStyle) {
|
||||
FlowRow(
|
||||
horizontalArrangement = ButtonsMainAxisSpacing,
|
||||
verticalArrangement = ButtonsCrossAxisSpacing
|
||||
) {
|
||||
dismissButton?.invoke()
|
||||
confirmButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,16 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.TipsAndUpdates
|
||||
import androidx.compose.material.icons.outlined.Call
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material.icons.outlined.TipsAndUpdates
|
||||
import androidx.compose.material.icons.outlined.ToggleOn
|
||||
import androidx.compose.material.icons.outlined.Translate
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -41,8 +43,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
@@ -57,8 +61,10 @@ import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.ui.monet.LocalTonalPalettes
|
||||
import org.xtimms.tokusho.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
||||
import org.xtimms.tokusho.ui.theme.PreviewThemeLight
|
||||
import org.xtimms.tokusho.ui.theme.TokushoTheme
|
||||
import org.xtimms.tokusho.ui.theme.applyOpacity
|
||||
import org.xtimms.tokusho.ui.theme.preferenceTitle
|
||||
import org.xtimms.tokusho.utils.FileSize
|
||||
|
||||
private const val horizontal = 8
|
||||
private const val vertical = 16
|
||||
@@ -516,7 +522,7 @@ fun PreferencesHintCard(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 16.dp)
|
||||
.padding(start = 8.dp, end = 24.dp)
|
||||
.size(24.dp),
|
||||
tint = contentColor
|
||||
)
|
||||
@@ -544,6 +550,160 @@ fun PreferencesHintCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceStorageHeader(
|
||||
used: Long = 4L,
|
||||
total: Long = 128L
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = FileSize.BYTES.formatWithoutUnits(used),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.displayLarge
|
||||
)
|
||||
Text(
|
||||
text = FileSize.BYTES.showUnit(LocalContext.current, used),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.Bottom)
|
||||
.padding(PaddingValues(bottom = 8.dp))
|
||||
)
|
||||
Text(
|
||||
text = FileSize.BYTES.totalFormat(LocalContext.current, total),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Bottom)
|
||||
.padding(PaddingValues(bottom = 8.dp))
|
||||
)
|
||||
}
|
||||
LinearProgressIndicator(
|
||||
progress = { (1 - ((total - used) / total.toFloat())) },
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp))
|
||||
.height(16.dp)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PreferenceStorageItem(
|
||||
title: String,
|
||||
used: Long? = 0L,
|
||||
total: Long?,
|
||||
icon: Any? = null,
|
||||
leadingIcon: (@Composable () -> Unit)? = null,
|
||||
trailingIcon: (@Composable () -> Unit)? = null,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.combinedClickable(
|
||||
onClick = onClick,
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal.dp, vertical.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
leadingIcon?.invoke()
|
||||
|
||||
when (icon) {
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 16.dp)
|
||||
.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.applyOpacity(true),
|
||||
)
|
||||
}
|
||||
|
||||
is Painter -> {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 16.dp)
|
||||
.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.applyOpacity(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (icon != null)
|
||||
Modifier
|
||||
.padding(start = 16.dp, end = 8.dp)
|
||||
else Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
) {
|
||||
Row {
|
||||
PreferenceItemTitle(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = title,
|
||||
enabled = true
|
||||
)
|
||||
Text(text = FileSize.BYTES.format(LocalContext.current, used ?: 0L))
|
||||
}
|
||||
if (total != null) {
|
||||
LinearProgressIndicator(
|
||||
progress = { (1 - ((total - used!!) / total.toFloat())) },
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(top = 12.dp))
|
||||
.height(5.dp)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
}
|
||||
trailingIcon?.let {
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.height(32.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
|
||||
thickness = 1.dp
|
||||
)
|
||||
trailingIcon.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun PreferenceStorageHeaderPreview() {
|
||||
TokushoTheme {
|
||||
PreferenceStorageHeader()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun PreferenceStorageItemPreview() {
|
||||
TokushoTheme {
|
||||
PreferenceStorageItem(title = "Saved manga", icon = Icons.Outlined.Save, total = 0L)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreferenceItemPreview() {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.DismissButton
|
||||
import org.xtimms.tokusho.core.updates.Updater
|
||||
import org.xtimms.tokusho.utils.system.suspendToast
|
||||
|
||||
@@ -88,10 +89,3 @@ fun UpdateDialogImpl(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DismissButton(text: String = stringResource(R.string.dismiss), onClick: () -> Unit) {
|
||||
TextButton(onClick = onClick) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,13 @@ import android.os.StatFs
|
||||
import androidx.annotation.WorkerThread
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.xtimms.tokusho.core.cache.CacheDir
|
||||
import org.xtimms.tokusho.utils.system.computeSize
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -28,6 +34,65 @@ class LocalStorageManager @Inject constructor(
|
||||
return Cache(directory, maxSize)
|
||||
}
|
||||
|
||||
suspend fun computeCacheSize(cache: CacheDir) = withContext(Dispatchers.IO) {
|
||||
getCacheDirs(cache.dir).sumOf { it.computeSize() }
|
||||
}
|
||||
|
||||
suspend fun computeCacheSize() = withContext(Dispatchers.IO) {
|
||||
getCacheDirs().sumOf { it.computeSize() }
|
||||
}
|
||||
|
||||
suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) {
|
||||
getAvailableStorageDirs().mapToSet { it.freeSpace }.sum()
|
||||
}
|
||||
|
||||
fun getAvailableStorageSpace(file: File): Long {
|
||||
return try {
|
||||
val stat = StatFs(file.absolutePath)
|
||||
stat.availableBlocksLong * stat.blockSizeLong
|
||||
} catch (_: Exception) {
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) {
|
||||
getCacheDirs(cache.dir).forEach { it.deleteRecursively() }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getAvailableStorageDirs(): MutableSet<File> {
|
||||
val result = LinkedHashSet<File>()
|
||||
result += File(context.filesDir, DIR_NAME)
|
||||
context.getExternalFilesDirs(DIR_NAME).filterNotNullTo(result)
|
||||
result.retainAll { it.exists() || it.mkdirs() }
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getFallbackStorageDir(): File? {
|
||||
return context.getExternalFilesDir(DIR_NAME) ?: File(context.filesDir, DIR_NAME).takeIf {
|
||||
it.exists() || it.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getCacheDirs(subDir: String): MutableSet<File> {
|
||||
val result = LinkedHashSet<File>()
|
||||
result += File(context.cacheDir, subDir)
|
||||
context.externalCacheDirs.mapNotNullTo(result) {
|
||||
File(it ?: return@mapNotNullTo null, subDir)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getCacheDirs(): MutableSet<File> {
|
||||
val result = LinkedHashSet<File>()
|
||||
result += context.cacheDir
|
||||
context.externalCacheDirs.filterNotNullTo(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
|
||||
return try {
|
||||
val cacheDir = StatFs(cacheDirectory.absolutePath)
|
||||
|
||||
@@ -6,12 +6,19 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Palette
|
||||
import androidx.compose.material.icons.outlined.Storage
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
|
||||
import org.xtimms.tokusho.core.components.SettingItem
|
||||
import org.xtimms.tokusho.sections.settings.storage.StorageViewModel
|
||||
import org.xtimms.tokusho.utils.FileSize
|
||||
|
||||
const val SETTINGS_DESTINATION = "settings"
|
||||
|
||||
@@ -20,8 +27,14 @@ fun SettingsView(
|
||||
navigateBack: () -> Unit,
|
||||
navigateToAppearance: () -> Unit,
|
||||
navigateToAbout: () -> Unit,
|
||||
navigateToAdvanced: () -> Unit
|
||||
navigateToAdvanced: () -> Unit,
|
||||
navigateToStorage: () -> Unit
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val viewModel: StorageViewModel = hiltViewModel()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.settings),
|
||||
navigateBack = navigateBack
|
||||
@@ -38,6 +51,32 @@ fun SettingsView(
|
||||
onClick = navigateToAppearance
|
||||
)
|
||||
}
|
||||
item {
|
||||
val allCaches = uiState.availableSpace +
|
||||
uiState.httpCacheSize +
|
||||
uiState.pagesCache +
|
||||
uiState.thumbnailsCache
|
||||
val desc = buildString {
|
||||
append((uiState.availableSpace / allCaches) * 100)
|
||||
append("% used")
|
||||
append(" - ")
|
||||
append(
|
||||
FileSize.BYTES.freeFormat(
|
||||
context,
|
||||
uiState.availableSpace -
|
||||
uiState.httpCacheSize -
|
||||
uiState.pagesCache -
|
||||
uiState.thumbnailsCache
|
||||
)
|
||||
)
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(id = R.string.storage),
|
||||
description = desc,
|
||||
icon = Icons.Outlined.Storage,
|
||||
onClick = navigateToStorage
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingItem(
|
||||
title = stringResource(id = R.string.advanced),
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.xtimms.tokusho.sections.settings.storage
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CleaningServices
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.ConfirmButton
|
||||
import org.xtimms.tokusho.core.components.DialogCheckBoxItem
|
||||
import org.xtimms.tokusho.core.components.DismissButton
|
||||
import org.xtimms.tokusho.core.components.TokushoDialog
|
||||
import org.xtimms.tokusho.utils.FileSize
|
||||
|
||||
@Composable
|
||||
fun CleanDialog(
|
||||
onDismissRequest: () -> Unit = {},
|
||||
isPagesCacheSelected: Boolean,
|
||||
isThumbnailsCacheSelected: Boolean,
|
||||
isNetworkCacheSelected: Boolean,
|
||||
onConfirm: (isPagesCacheSelected: Boolean, isThumbnailCacheSelected: Boolean, isNetworkCacheSelected: Boolean) -> Unit = { _, _, _ -> }
|
||||
) {
|
||||
|
||||
val viewModel: StorageViewModel = hiltViewModel()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
var pagesCache by remember {
|
||||
mutableStateOf(isPagesCacheSelected)
|
||||
}
|
||||
var thumbnailsCache by remember {
|
||||
mutableStateOf(isThumbnailsCacheSelected)
|
||||
}
|
||||
var networkCache by remember {
|
||||
mutableStateOf(isNetworkCacheSelected)
|
||||
}
|
||||
|
||||
TokushoDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
ConfirmButton {
|
||||
onConfirm(pagesCache, thumbnailsCache, networkCache)
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
DismissButton {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.free_up_space
|
||||
)
|
||||
)
|
||||
},
|
||||
icon = { Icon(imageVector = Icons.Outlined.CleaningServices, contentDescription = null) },
|
||||
text = {
|
||||
Column {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp))
|
||||
DialogCheckBoxItem(
|
||||
text = stringResource(id = R.string.pages_cache),
|
||||
checked = pagesCache
|
||||
) {
|
||||
pagesCache = !pagesCache
|
||||
}
|
||||
DialogCheckBoxItem(
|
||||
text = stringResource(id = R.string.thumbnails_cache),
|
||||
checked = thumbnailsCache
|
||||
) {
|
||||
thumbnailsCache = !thumbnailsCache
|
||||
}
|
||||
DialogCheckBoxItem(
|
||||
text = stringResource(id = R.string.network_cache),
|
||||
checked = networkCache
|
||||
) {
|
||||
networkCache = !networkCache
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
val summary = StringBuilder().run {
|
||||
append(FileSize.BYTES.format(LocalContext.current, uiState.pagesCache + uiState.thumbnailsCache + uiState.httpCacheSize))
|
||||
append("")
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.free_up_space_summary) + " " + summary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
// style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DirectoryPreferenceDialogPreview() {
|
||||
CleanDialog(
|
||||
onDismissRequest = {},
|
||||
isPagesCacheSelected = false,
|
||||
isThumbnailsCacheSelected = false,
|
||||
isNetworkCacheSelected = false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.xtimms.tokusho.sections.settings.storage
|
||||
|
||||
import org.xtimms.tokusho.core.base.state.UiState
|
||||
|
||||
data class StorageUiState(
|
||||
val pagesCache: Long = -1L,
|
||||
val thumbnailsCache: Long = -1L,
|
||||
val availableSpace: Long = -1L,
|
||||
val httpCacheSize: Long = -1L,
|
||||
override val isLoading: Boolean = false,
|
||||
override val message: String? = null,
|
||||
) : UiState() {
|
||||
override fun setLoading(value: Boolean) = copy(isLoading = value)
|
||||
override fun setMessage(value: String?) = copy(message = value)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.xtimms.tokusho.sections.settings.storage
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.CleaningServices
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.NetworkWifi
|
||||
import androidx.compose.material.icons.outlined.SdStorage
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.cache.CacheDir
|
||||
import org.xtimms.tokusho.core.components.PreferenceStorageHeader
|
||||
import org.xtimms.tokusho.core.components.PreferenceStorageItem
|
||||
import org.xtimms.tokusho.core.components.PreferencesHintCard
|
||||
import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
|
||||
|
||||
const val STORAGE_DESTINATION = "storage"
|
||||
|
||||
@Composable
|
||||
fun StorageView(
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
|
||||
val viewModel: StorageViewModel = hiltViewModel()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
var showCleanDialog by remember { mutableStateOf(false) }
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.storage),
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
if (!uiState.isLoading) LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
) {
|
||||
item {
|
||||
PreferenceStorageHeader(
|
||||
used = uiState.httpCacheSize + uiState.thumbnailsCache + uiState.pagesCache,
|
||||
total = uiState.availableSpace
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferencesHintCard(
|
||||
title = stringResource(id = R.string.free_up_space),
|
||||
description = stringResource(id = R.string.free_up_space_hint),
|
||||
icon = Icons.Outlined.CleaningServices
|
||||
) {
|
||||
showCleanDialog = true
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceStorageItem(
|
||||
total = uiState.availableSpace,
|
||||
title = stringResource(id = R.string.saved_manga),
|
||||
icon = Icons.Outlined.SdStorage
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceStorageItem(
|
||||
total = uiState.availableSpace,
|
||||
title = stringResource(id = R.string.pages_cache),
|
||||
icon = Icons.Outlined.AutoStories,
|
||||
used = uiState.pagesCache
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceStorageItem(
|
||||
total = uiState.availableSpace,
|
||||
title = stringResource(id = R.string.thumbnails_cache),
|
||||
icon = Icons.Outlined.Image,
|
||||
used = uiState.thumbnailsCache
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceStorageItem(
|
||||
total = uiState.availableSpace,
|
||||
title = stringResource(id = R.string.network_cache),
|
||||
icon = Icons.Outlined.NetworkWifi,
|
||||
used = uiState.httpCacheSize
|
||||
)
|
||||
}
|
||||
} else Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
if (showCleanDialog) {
|
||||
CleanDialog(
|
||||
onDismissRequest = { showCleanDialog = false },
|
||||
isPagesCacheSelected = false,
|
||||
isNetworkCacheSelected = false,
|
||||
isThumbnailsCacheSelected = false,
|
||||
onConfirm = { isPagesCacheSelected, isThumbnailCacheSelected, isNetworkCacheSelected ->
|
||||
if (isPagesCacheSelected) viewModel.clearCache(CacheDir.PAGES)
|
||||
if (isThumbnailCacheSelected) viewModel.clearCache(CacheDir.THUMBS)
|
||||
if (isNetworkCacheSelected) viewModel.clearHttpCache()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.xtimms.tokusho.sections.settings.storage
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Cache
|
||||
import org.xtimms.tokusho.core.base.viewmodel.BaseViewModel
|
||||
import org.xtimms.tokusho.core.cache.CacheDir
|
||||
import org.xtimms.tokusho.data.LocalStorageManager
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StorageViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val httpCache: Cache,
|
||||
) : BaseViewModel<StorageUiState>() {
|
||||
|
||||
val httpCacheSize = MutableStateFlow(-1L)
|
||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
setLoading(true)
|
||||
httpCacheSize.value = runInterruptible { httpCache.size() }
|
||||
mutableUiState.update {
|
||||
it.copy(
|
||||
availableSpace = storageManager.computeAvailableSize(),
|
||||
pagesCache = storageManager.computeCacheSize(CacheDir.PAGES),
|
||||
thumbnailsCache = storageManager.computeCacheSize(CacheDir.THUMBS),
|
||||
httpCacheSize = httpCacheSize.value,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache(cache: CacheDir) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
mutableUiState.update {
|
||||
it.copy(
|
||||
availableSpace = storageManager.computeAvailableSize(),
|
||||
pagesCache = storageManager.computeCacheSize(CacheDir.PAGES),
|
||||
thumbnailsCache = storageManager.computeCacheSize(CacheDir.THUMBS),
|
||||
httpCacheSize = httpCacheSize.value,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHttpCache() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
val size = runInterruptible(Dispatchers.IO) {
|
||||
httpCache.evictAll()
|
||||
httpCache.size()
|
||||
}
|
||||
httpCacheSize.value = size
|
||||
mutableUiState.update {
|
||||
it.copy(
|
||||
availableSpace = storageManager.computeAvailableSize(),
|
||||
pagesCache = storageManager.computeCacheSize(CacheDir.PAGES),
|
||||
thumbnailsCache = storageManager.computeCacheSize(CacheDir.THUMBS),
|
||||
httpCacheSize = httpCacheSize.value,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val mutableUiState = MutableStateFlow(StorageUiState())
|
||||
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import android.os.Build
|
||||
import org.xtimms.tokusho.BuildConfig
|
||||
import org.xtimms.tokusho.utils.lang.withNonCancellableContext
|
||||
import org.xtimms.tokusho.utils.lang.withUIContext
|
||||
import org.xtimms.tokusho.utils.storage.getUriCompat
|
||||
import org.xtimms.tokusho.utils.system.createFileInCacheDir
|
||||
import org.xtimms.tokusho.utils.system.getUriCompat
|
||||
import org.xtimms.tokusho.utils.system.toShareIntent
|
||||
import org.xtimms.tokusho.utils.system.toast
|
||||
|
||||
|
||||
108
app/src/main/java/org/xtimms/tokusho/utils/FileSize.kt
Normal file
108
app/src/main/java/org/xtimms/tokusho/utils/FileSize.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package org.xtimms.tokusho.utils
|
||||
|
||||
import android.content.Context
|
||||
import org.xtimms.tokusho.R
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
enum class FileSize(private val multiplier: Int) {
|
||||
|
||||
BYTES(1), KILOBYTES(1024), MEGABYTES(1024 * 1024);
|
||||
|
||||
fun convert(amount: Long, target: FileSize): Long = amount * multiplier / target.multiplier
|
||||
|
||||
fun freeFormat(context: Context, amount: Long): String {
|
||||
val bytes = amount * multiplier
|
||||
val units = context.getString(R.string.text_file_sizes_free).split('|')
|
||||
if (bytes <= 0) {
|
||||
return "0 ${units.first()}"
|
||||
}
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
return buildString {
|
||||
append(
|
||||
DecimalFormat("#,##0.#").format(
|
||||
bytes / 1024.0.pow(digitGroups.toDouble()),
|
||||
),
|
||||
)
|
||||
val unit = units.getOrNull(digitGroups)
|
||||
if (unit != null) {
|
||||
append(' ')
|
||||
append(unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun totalFormat(context: Context, amount: Long): String {
|
||||
val bytes = amount * multiplier
|
||||
val units = context.getString(R.string.text_file_sizes_total).split('|')
|
||||
if (bytes <= 0) {
|
||||
return "0 ${units.first()}"
|
||||
}
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
return buildString {
|
||||
append(
|
||||
DecimalFormat("#,##0.#").format(
|
||||
bytes / 1024.0.pow(digitGroups.toDouble()),
|
||||
),
|
||||
)
|
||||
val unit = units.getOrNull(digitGroups)
|
||||
if (unit != null) {
|
||||
append(' ')
|
||||
append(unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showUnit(context: Context, amount: Long): String {
|
||||
val bytes = amount * multiplier
|
||||
val units = context.getString(R.string.text_file_sizes_used).split('|')
|
||||
if (bytes <= 0) {
|
||||
return units.first()
|
||||
}
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
return buildString {
|
||||
val unit = units.getOrNull(digitGroups)
|
||||
if (unit != null) {
|
||||
append(' ')
|
||||
append(unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun format(context: Context, amount: Long): String {
|
||||
val bytes = amount * multiplier
|
||||
val units = context.getString(R.string.text_file_sizes).split('|')
|
||||
if (bytes <= 0) {
|
||||
return "0 ${units.first()}"
|
||||
}
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
return buildString {
|
||||
append(
|
||||
DecimalFormat("#,##0.#").format(
|
||||
bytes / 1024.0.pow(digitGroups.toDouble()),
|
||||
),
|
||||
)
|
||||
val unit = units.getOrNull(digitGroups)
|
||||
if (unit != null) {
|
||||
append(' ')
|
||||
append(unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatWithoutUnits(amount: Long): String {
|
||||
val bytes = amount * multiplier
|
||||
if (bytes <= 0) {
|
||||
return "0"
|
||||
}
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
return buildString {
|
||||
append(
|
||||
DecimalFormat("#,##0.#").format(
|
||||
bytes / 1024.0.pow(digitGroups.toDouble()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.xtimms.tokusho.utils.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import org.xtimms.tokusho.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Returns the uri of a file
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
fun File.getUriCompat(context: Context): Uri {
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
package org.xtimms.tokusho.utils.system
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.xtimms.tokusho.BuildConfig
|
||||
import java.io.File
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.walk
|
||||
|
||||
fun File.getUriCompat(context: Context): Uri {
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
|
||||
}
|
||||
|
||||
fun Context.getFileProvider() = "$packageName.provider"
|
||||
|
||||
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
|
||||
walkCompat().sumOf { it.length() }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun File.walkCompat() =
|
||||
// Use lazy loading on Android 8.0 and later
|
||||
toPath().walk().map { it.toFile() }
|
||||
|
||||
@@ -80,4 +80,19 @@
|
||||
<string name="webview_version">WebView version</string>
|
||||
<string name="advanced_page">Dump crash logs, debug info</string>
|
||||
<string name="device_info">Device info</string>
|
||||
<string name="storage">Storage</string>
|
||||
<string name="free_up_space">Free up space</string>
|
||||
<string name="free_up_space_hint">Choose what needs to be cleared</string>
|
||||
<string name="saved_manga">Saved manga</string>
|
||||
<string name="pages_cache">Pages cache</string>
|
||||
<string name="other_cache">Other cache</string>
|
||||
<string name="thumbnails_cache">Thumbnails cache</string>
|
||||
<string name="network_cache">Network cache</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="computing_">Computing...</string>
|
||||
<string name="text_file_sizes_total">B total|kB total|MB total|GB total|TB total</string>
|
||||
<string name="text_file_sizes_used">B used|kB used|MB used|GB used|TB used</string>
|
||||
<string name="text_file_sizes_free">B free|kB free|MB free|GB free|TB free</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="free_up_space_summary">Space can be cleared:</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user