Storage screen
This commit is contained in:
@@ -84,8 +84,8 @@ dependencies {
|
|||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.6.0")
|
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-android:1.2.0")
|
||||||
implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
|
implementation("androidx.compose.material3:material3-window-size-class:1.2.0")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
implementation("androidx.navigation:navigation-compose:2.7.6")
|
||||||
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||||
@@ -94,6 +94,7 @@ dependencies {
|
|||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
ksp("androidx.room:room-compiler:2.6.1")
|
ksp("androidx.room:room-compiler:2.6.1")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
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-systemuicontroller:0.32.0")
|
||||||
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
||||||
implementation("com.google.accompanist:accompanist-pager-indicators: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.DarkThemeView
|
||||||
import org.xtimms.tokusho.sections.settings.appearance.LANGUAGES_DESTINATION
|
import org.xtimms.tokusho.sections.settings.appearance.LANGUAGES_DESTINATION
|
||||||
import org.xtimms.tokusho.sections.settings.appearance.LanguagesView
|
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.ShelfMap
|
||||||
import org.xtimms.tokusho.sections.shelf.ShelfView
|
import org.xtimms.tokusho.sections.shelf.ShelfView
|
||||||
import org.xtimms.tokusho.utils.lang.removeFirstAndLast
|
import org.xtimms.tokusho.utils.lang.removeFirstAndLast
|
||||||
@@ -144,7 +146,8 @@ fun Navigation(
|
|||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
navigateToAppearance = { navController.navigate(APPEARANCE_DESTINATION) },
|
navigateToAppearance = { navController.navigate(APPEARANCE_DESTINATION) },
|
||||||
navigateToAbout = { navController.navigate(ABOUT_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) {
|
composable(ADVANCED_DESTINATION) {
|
||||||
AdvancedView(
|
AdvancedView(
|
||||||
navigateBack = navigateBack,
|
navigateBack = navigateBack,
|
||||||
|
|||||||
@@ -9,8 +9,28 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
fun ActionButton(
|
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.selectable
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.material.icons.Icons
|
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.Call
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
import androidx.compose.material.icons.outlined.Info
|
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.ToggleOn
|
||||||
import androidx.compose.material.icons.outlined.Translate
|
import androidx.compose.material.icons.outlined.Translate
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -41,8 +43,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
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.LocalTonalPalettes
|
||||||
import org.xtimms.tokusho.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
import org.xtimms.tokusho.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
||||||
import org.xtimms.tokusho.ui.theme.PreviewThemeLight
|
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.applyOpacity
|
||||||
import org.xtimms.tokusho.ui.theme.preferenceTitle
|
import org.xtimms.tokusho.ui.theme.preferenceTitle
|
||||||
|
import org.xtimms.tokusho.utils.FileSize
|
||||||
|
|
||||||
private const val horizontal = 8
|
private const val horizontal = 8
|
||||||
private const val vertical = 16
|
private const val vertical = 16
|
||||||
@@ -516,7 +522,7 @@ fun PreferencesHintCard(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 8.dp, end = 16.dp)
|
.padding(start = 8.dp, end = 24.dp)
|
||||||
.size(24.dp),
|
.size(24.dp),
|
||||||
tint = contentColor
|
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
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun PreferenceItemPreview() {
|
fun PreferenceItemPreview() {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.xtimms.tokusho.R
|
import org.xtimms.tokusho.R
|
||||||
|
import org.xtimms.tokusho.core.components.DismissButton
|
||||||
import org.xtimms.tokusho.core.updates.Updater
|
import org.xtimms.tokusho.core.updates.Updater
|
||||||
import org.xtimms.tokusho.utils.system.suspendToast
|
import org.xtimms.tokusho.utils.system.suspendToast
|
||||||
|
|
||||||
@@ -87,11 +88,4 @@ fun UpdateDialogImpl(
|
|||||||
Text(releaseNote)
|
Text(releaseNote)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
@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 androidx.annotation.WorkerThread
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Cache
|
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 java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -28,6 +34,65 @@ class LocalStorageManager @Inject constructor(
|
|||||||
return Cache(directory, maxSize)
|
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 {
|
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
|
||||||
return try {
|
return try {
|
||||||
val cacheDir = StatFs(cacheDirectory.absolutePath)
|
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.Code
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Palette
|
import androidx.compose.material.icons.outlined.Palette
|
||||||
|
import androidx.compose.material.icons.outlined.Storage
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
|
import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar
|
||||||
import org.xtimms.tokusho.core.components.SettingItem
|
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"
|
const val SETTINGS_DESTINATION = "settings"
|
||||||
|
|
||||||
@@ -20,8 +27,14 @@ fun SettingsView(
|
|||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
navigateToAppearance: () -> Unit,
|
navigateToAppearance: () -> Unit,
|
||||||
navigateToAbout: () -> Unit,
|
navigateToAbout: () -> Unit,
|
||||||
navigateToAdvanced: () -> Unit
|
navigateToAdvanced: () -> Unit,
|
||||||
|
navigateToStorage: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel: StorageViewModel = hiltViewModel()
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ScaffoldWithTopAppBar(
|
ScaffoldWithTopAppBar(
|
||||||
title = stringResource(R.string.settings),
|
title = stringResource(R.string.settings),
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
@@ -38,6 +51,32 @@ fun SettingsView(
|
|||||||
onClick = navigateToAppearance
|
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 {
|
item {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(id = R.string.advanced),
|
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.BuildConfig
|
||||||
import org.xtimms.tokusho.utils.lang.withNonCancellableContext
|
import org.xtimms.tokusho.utils.lang.withNonCancellableContext
|
||||||
import org.xtimms.tokusho.utils.lang.withUIContext
|
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.createFileInCacheDir
|
||||||
|
import org.xtimms.tokusho.utils.system.getUriCompat
|
||||||
import org.xtimms.tokusho.utils.system.toShareIntent
|
import org.xtimms.tokusho.utils.system.toShareIntent
|
||||||
import org.xtimms.tokusho.utils.system.toast
|
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
|
package org.xtimms.tokusho.utils.system
|
||||||
|
|
||||||
import android.content.Context
|
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 Context.getFileProvider() = "$packageName.provider"
|
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="webview_version">WebView version</string>
|
||||||
<string name="advanced_page">Dump crash logs, debug info</string>
|
<string name="advanced_page">Dump crash logs, debug info</string>
|
||||||
<string name="device_info">Device 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>
|
</resources>
|
||||||
Reference in New Issue
Block a user