Compare commits
No commits in common. '774bb84f7f3e7a3b7b65aeb7f27fbe6b58fd330d' and '746934d421a1ddbfcd9d65b47f5fc1cd9c749aac' have entirely different histories.
774bb84f7f
...
746934d421
@ -1,57 +0,0 @@
|
||||
package org.xtimms.shirizu.core.database
|
||||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ShirizuDatabaseTest {
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
ShirizuDatabase::class.java,
|
||||
)
|
||||
|
||||
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
|
||||
@Test
|
||||
fun versions() {
|
||||
assertEquals(1, migrations.first().startVersion)
|
||||
repeat(migrations.size) { i ->
|
||||
assertEquals(i + 1, migrations[i].startVersion)
|
||||
assertEquals(i + 2, migrations[i].endVersion)
|
||||
}
|
||||
assertEquals(DATABASE_VERSION, migrations.last().endVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateAll() {
|
||||
helper.createDatabase(TEST_DB, 1).close()
|
||||
for (migration in migrations) {
|
||||
helper.runMigrationsAndValidate(
|
||||
TEST_DB,
|
||||
migration.endVersion,
|
||||
true,
|
||||
migration,
|
||||
).close()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prePopulate() {
|
||||
val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources
|
||||
helper.createDatabase(TEST_DB, DATABASE_VERSION).use {
|
||||
DatabasePrePopulateCallback(resources).onCreate(it)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TEST_DB = "test-db"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
package org.xtimms.shirizu.core
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
|
||||
object HapticFeedback {
|
||||
fun View.slightHapticFeedback() =
|
||||
this.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
||||
|
||||
fun View.longPressHapticFeedback() =
|
||||
this.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.shirizu.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(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OutlinedButtonWithIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
contentColor: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor)
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextButtonWithIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
contentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = contentColor)
|
||||
)
|
||||
{
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(18.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilledTonalButtonWithIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
colors = colors
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.size(18.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilledButtonWithIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||
enabled = enabled
|
||||
)
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.size(18.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SingleChoiceChip(
|
||||
modifier: Modifier = Modifier,
|
||||
selected: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
leadingIcon: ImageVector = Icons.Outlined.Check
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = modifier.padding(horizontal = 4.dp),
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
label = {
|
||||
Text(text = label)
|
||||
},
|
||||
leadingIcon = {
|
||||
Row {
|
||||
AnimatedVisibility(visible = selected) {
|
||||
Icon(
|
||||
imageVector = leadingIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,222 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun DialogSingleChoiceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectable(
|
||||
selected = selected,
|
||||
enabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.clearAndSetSemantics { },
|
||||
selected = selected,
|
||||
onClick = onClick
|
||||
)
|
||||
Text(text = text, style = LocalTextStyle.current.copy(fontSize = 16.sp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SingleChoiceItemPreview() {
|
||||
Surface {
|
||||
Column {
|
||||
DialogSingleChoiceItemWithLabel(
|
||||
text = "Better compatibility", label = "For sharing to other apps", selected = false
|
||||
) {
|
||||
|
||||
}
|
||||
DialogSingleChoiceItemWithLabel(
|
||||
text = "Better quality", label = "For watching in compatible apps", selected = true
|
||||
) {
|
||||
|
||||
}
|
||||
DialogSingleChoiceItem(text = "Preview", selected = true) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogSingleChoiceItemWithLabel(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
label: String?,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.selectable(
|
||||
selected = selected,
|
||||
enabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, end = 16.dp)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.clearAndSetSemantics { },
|
||||
selected = selected,
|
||||
onClick = onClick
|
||||
)
|
||||
Column {
|
||||
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
||||
label?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckBoxItem(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = checked, enabled = true, onValueChange = onValueChange
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
checked = checked, onCheckedChange = onValueChange,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier, text = text, style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DialogSwitchItem(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(value = value, onValueChange = onValueChange)
|
||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
val thumbContent: (@Composable () -> Unit)? = remember(value) {
|
||||
if (value) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides Density(
|
||||
density.density * 0.8f,
|
||||
density.fontScale
|
||||
)
|
||||
) {
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = onValueChange,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
thumbContent = thumbContent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SwitchItemPrev() {
|
||||
var value by remember { mutableStateOf(false) }
|
||||
Surface {
|
||||
DialogSwitchItem(text = "Use cookies", value = value) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,327 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SignalCellularConnectedNoInternet4Bar
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.ui.theme.FixedAccentColors
|
||||
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||
|
||||
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 ShirizuDialog(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShirizuDialogButtonVariant(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = MiddleButtonShape,
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box() {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
color = FixedAccentColors.secondaryFixed,
|
||||
shape = shape
|
||||
) {
|
||||
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = FixedAccentColors.onSecondaryFixed,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "light", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Composable
|
||||
private fun ButtonVariantPreview() {
|
||||
ShirizuTheme {
|
||||
ShirizuDialogVariant(
|
||||
onDismissRequest = {}, modifier = Modifier,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SignalCellularConnectedNoInternet4Bar,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = "Download with cellular network?",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
buttons = {
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(R.string.allow_always),
|
||||
shape = TopButtonShape
|
||||
) {}
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(id = R.string.allow_once),
|
||||
shape = MiddleButtonShape
|
||||
) {}
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(R.string.dont_allow),
|
||||
shape = BottomButtonShape
|
||||
) {}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val TopButtonShape = RoundedCornerShape(
|
||||
topStart = 12.dp,
|
||||
topEnd = 12.dp,
|
||||
bottomStart = 4.dp,
|
||||
bottomEnd = 4.dp
|
||||
)
|
||||
|
||||
val MiddleButtonShape = RoundedCornerShape(4.dp)
|
||||
|
||||
val BottomButtonShape = RoundedCornerShape(
|
||||
topStart = 4.dp,
|
||||
topEnd = 4.dp,
|
||||
bottomStart = 12.dp,
|
||||
bottomEnd = 12.dp
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShirizuDialogVariant(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
buttons: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = AlertDialogDefaults.shape,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
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.copy(textAlign = TextAlign.Center)) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(DialogHorizontalPadding)
|
||||
) {
|
||||
buttons?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShirizuModalBottomSheet(
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
density = LocalDensity.current,
|
||||
initialValue = SheetValue.Hidden
|
||||
),
|
||||
onDismissRequest: () -> Unit,
|
||||
horizontalPadding: PaddingValues = PaddingValues(horizontal = 28.dp),
|
||||
content: @Composable ColumnScope.() -> Unit = {},
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
windowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(paddingValues = horizontalPadding)) {
|
||||
content()
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
}
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
.fillMaxWidth()
|
||||
.height(
|
||||
with(
|
||||
WindowInsets.navigationBars
|
||||
.asPaddingValues()
|
||||
.calculateBottomPadding()
|
||||
) {
|
||||
when {
|
||||
this.value > 30f -> {
|
||||
this
|
||||
}
|
||||
|
||||
// FIXME: https://issuetracker.google.com/issues/290798798
|
||||
Build.VERSION.SDK_INT < 30 -> {
|
||||
48.dp
|
||||
}
|
||||
|
||||
else -> {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerSheetSubtitle(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
color: Color = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
|
||||
color = color,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
package org.xtimms.shirizu.core.components.icons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
public val Icons.Outlined.Creation: ImageVector
|
||||
get() {
|
||||
if (_creation != null) {
|
||||
return _creation!!
|
||||
}
|
||||
_creation = Builder(name = "Creation", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||
viewportWidth = 24.0f, viewportHeight = 24.0f).apply {
|
||||
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero) {
|
||||
moveTo(9.0f, 4.0f)
|
||||
lineTo(11.5f, 9.5f)
|
||||
lineTo(17.0f, 12.0f)
|
||||
lineTo(11.5f, 14.5f)
|
||||
lineTo(9.0f, 20.0f)
|
||||
lineTo(6.5f, 14.5f)
|
||||
lineTo(1.0f, 12.0f)
|
||||
lineTo(6.5f, 9.5f)
|
||||
lineTo(9.0f, 4.0f)
|
||||
moveTo(9.0f, 8.83f)
|
||||
lineTo(8.0f, 11.0f)
|
||||
lineTo(5.83f, 12.0f)
|
||||
lineTo(8.0f, 13.0f)
|
||||
lineTo(9.0f, 15.17f)
|
||||
lineTo(10.0f, 13.0f)
|
||||
lineTo(12.17f, 12.0f)
|
||||
lineTo(10.0f, 11.0f)
|
||||
lineTo(9.0f, 8.83f)
|
||||
moveTo(19.0f, 9.0f)
|
||||
lineTo(17.74f, 6.26f)
|
||||
lineTo(15.0f, 5.0f)
|
||||
lineTo(17.74f, 3.75f)
|
||||
lineTo(19.0f, 1.0f)
|
||||
lineTo(20.25f, 3.75f)
|
||||
lineTo(23.0f, 5.0f)
|
||||
lineTo(20.25f, 6.26f)
|
||||
lineTo(19.0f, 9.0f)
|
||||
moveTo(19.0f, 23.0f)
|
||||
lineTo(17.74f, 20.26f)
|
||||
lineTo(15.0f, 19.0f)
|
||||
lineTo(17.74f, 17.75f)
|
||||
lineTo(19.0f, 15.0f)
|
||||
lineTo(20.25f, 17.75f)
|
||||
lineTo(23.0f, 19.0f)
|
||||
lineTo(20.25f, 20.26f)
|
||||
lineTo(19.0f, 23.0f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return _creation!!
|
||||
}
|
||||
|
||||
private var _creation: ImageVector? = null
|
||||
@ -1,69 +0,0 @@
|
||||
package org.xtimms.shirizu.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.xtimms.shirizu.core.database.entity.MangaEntity
|
||||
import org.xtimms.shirizu.core.database.entity.StatsEntity
|
||||
|
||||
@Dao
|
||||
abstract class StatsDao {
|
||||
|
||||
@Query("SELECT * FROM stats ORDER BY started_at")
|
||||
abstract suspend fun findAll(): List<StatsEntity>
|
||||
|
||||
@Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at")
|
||||
abstract suspend fun findAll(mangaId: Long): List<StatsEntity>
|
||||
|
||||
@Query("SELECT IFNULL(SUM(pages),0) FROM stats WHERE manga_id = :mangaId")
|
||||
abstract suspend fun getReadPagesCount(mangaId: Long): Int
|
||||
|
||||
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats WHERE manga_id = :mangaId")
|
||||
abstract suspend fun getAverageTimePerPage(mangaId: Long): Long
|
||||
|
||||
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats")
|
||||
abstract suspend fun getAverageTimePerPage(): Long
|
||||
|
||||
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats WHERE manga_id = :mangaId")
|
||||
abstract suspend fun getReadingTime(mangaId: Long): Long
|
||||
|
||||
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
|
||||
abstract suspend fun getTotalReadingTime(): Long
|
||||
|
||||
@Query("DELETE FROM stats")
|
||||
abstract suspend fun clear()
|
||||
|
||||
@Query("SELECT COUNT(*) FROM stats WHERE manga_id = :mangaId")
|
||||
abstract fun observeRowCount(mangaId: Long): Flow<Int>
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entity: StatsEntity)
|
||||
|
||||
suspend fun getDurationStats(fromDate: Long, isNsfw: Boolean?, favouriteCategories: Set<Long>): Map<MangaEntity, Long> {
|
||||
val conditions = ArrayList<String>()
|
||||
conditions.add("stats.started_at >= $fromDate")
|
||||
if (favouriteCategories.isNotEmpty()) {
|
||||
val ids = favouriteCategories.joinToString(",")
|
||||
conditions.add("stats.manga_id IN (SELECT manga_id FROM favourites WHERE category_id IN ($ids))")
|
||||
}
|
||||
if (isNsfw != null) {
|
||||
val flag = if (isNsfw) 1 else 0
|
||||
conditions.add("manga.nsfw = $flag")
|
||||
}
|
||||
val where = conditions.joinToString(separator = " AND ")
|
||||
val query = SimpleSQLiteQuery(
|
||||
"SELECT manga.*, SUM(duration) AS d FROM stats LEFT JOIN manga ON manga.manga_id = stats.manga_id WHERE $where GROUP BY manga.manga_id ORDER BY d DESC",
|
||||
)
|
||||
return getDurationStatsImpl(query)
|
||||
}
|
||||
|
||||
@RawQuery
|
||||
protected abstract fun getDurationStatsImpl(
|
||||
query: SupportSQLiteQuery
|
||||
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
package org.xtimms.shirizu.core.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "stats",
|
||||
primaryKeys = ["manga_id", "started_at"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = HistoryEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class StatsEntity(
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "started_at") val startedAt: Long,
|
||||
@ColumnInfo(name = "duration") val duration: Long,
|
||||
@ColumnInfo(name = "pages") val pages: Int,
|
||||
)
|
||||
@ -1,11 +0,0 @@
|
||||
package org.xtimms.shirizu.core.database.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration1To2 : Migration(1, 2) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `stats` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
package org.xtimms.shirizu.core.network.doh
|
||||
|
||||
import okhttp3.Cache
|
||||
import okhttp3.Dns
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class DoHManager(
|
||||
cache: Cache,
|
||||
) : Dns {
|
||||
|
||||
private val bootstrapClient = OkHttpClient.Builder().cache(cache).build()
|
||||
|
||||
private var cachedDelegate: Dns? = null
|
||||
private var cachedProvider: Int? = null
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
return try {
|
||||
getDelegate().lookup(hostname)
|
||||
} catch (e: UnknownHostException) {
|
||||
// fallback
|
||||
Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getDelegate(): Dns {
|
||||
var delegate = cachedDelegate
|
||||
val provider = AppSettings.getDOH()
|
||||
if (delegate == null || provider != cachedProvider) {
|
||||
delegate = createDelegate(provider)
|
||||
cachedDelegate = delegate
|
||||
cachedProvider = provider
|
||||
}
|
||||
return delegate
|
||||
}
|
||||
|
||||
private fun createDelegate(provider: Int): Dns = when (provider) {
|
||||
DoHProvider.NONE.ordinal -> Dns.SYSTEM
|
||||
DoHProvider.GOOGLE.ordinal -> DnsOverHttps.Builder().client(bootstrapClient)
|
||||
.url("https://dns.google/dns-query".toHttpUrl())
|
||||
.resolvePrivateAddresses(true)
|
||||
.bootstrapDnsHosts(
|
||||
listOfNotNull(
|
||||
tryGetByIp("8.8.4.4"),
|
||||
tryGetByIp("8.8.8.8"),
|
||||
tryGetByIp("2001:4860:4860::8888"),
|
||||
tryGetByIp("2001:4860:4860::8844"),
|
||||
),
|
||||
).build()
|
||||
|
||||
DoHProvider.CLOUDFLARE.ordinal -> DnsOverHttps.Builder().client(bootstrapClient)
|
||||
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||
.resolvePrivateAddresses(true)
|
||||
.bootstrapDnsHosts(
|
||||
listOfNotNull(
|
||||
tryGetByIp("162.159.36.1"),
|
||||
tryGetByIp("162.159.46.1"),
|
||||
tryGetByIp("1.1.1.1"),
|
||||
tryGetByIp("1.0.0.1"),
|
||||
tryGetByIp("162.159.132.53"),
|
||||
tryGetByIp("2606:4700:4700::1111"),
|
||||
tryGetByIp("2606:4700:4700::1001"),
|
||||
tryGetByIp("2606:4700:4700::0064"),
|
||||
tryGetByIp("2606:4700:4700::6400"),
|
||||
),
|
||||
).build()
|
||||
|
||||
DoHProvider.ADGUARD.ordinal -> DnsOverHttps.Builder().client(bootstrapClient)
|
||||
.url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl())
|
||||
.resolvePrivateAddresses(true)
|
||||
.bootstrapDnsHosts(
|
||||
listOfNotNull(
|
||||
tryGetByIp("94.140.14.140"),
|
||||
tryGetByIp("94.140.14.141"),
|
||||
tryGetByIp("2a10:50c0::1:ff"),
|
||||
tryGetByIp("2a10:50c0::2:ff"),
|
||||
),
|
||||
).build()
|
||||
|
||||
else -> { Dns.SYSTEM }
|
||||
}
|
||||
|
||||
private fun tryGetByIp(ip: String): InetAddress? = try {
|
||||
InetAddress.getByName(ip)
|
||||
} catch (e: UnknownHostException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package org.xtimms.shirizu.core.network.doh
|
||||
|
||||
enum class DoHProvider {
|
||||
NONE, GOOGLE, CLOUDFLARE, ADGUARD
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package org.xtimms.shirizu.core.network.proxy
|
||||
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.ProxySelector
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
|
||||
class AppProxySelector : ProxySelector() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
private var cachedProxy: Proxy? = null
|
||||
|
||||
override fun select(uri: URI?): List<Proxy> {
|
||||
return listOf(getProxy())
|
||||
}
|
||||
|
||||
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
|
||||
ioe?.printStackTrace()
|
||||
}
|
||||
|
||||
private fun getProxy(): Proxy {
|
||||
val type = AppSettings.getProxyType()
|
||||
val address = AppSettings.getProxyAddress()
|
||||
val port = AppSettings.getProxyPort()
|
||||
if (type == Proxy.Type.DIRECT.ordinal || address.isEmpty() || port == 0) {
|
||||
return Proxy.NO_PROXY
|
||||
}
|
||||
cachedProxy?.let {
|
||||
val addr = it.address() as? InetSocketAddress
|
||||
if (addr != null && it.type().ordinal == type && addr.port == port && addr.hostString == address) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
val proxy = Proxy(Proxy.Type.entries[type], InetSocketAddress(address, port))
|
||||
cachedProxy = proxy
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package org.xtimms.shirizu.core.network.proxy
|
||||
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.xtimms.shirizu.core.network.CommonHeaders
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.Proxy
|
||||
|
||||
class ProxyAuthenticator: Authenticator, java.net.Authenticator() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
|
||||
return null
|
||||
}
|
||||
val login = AppSettings.getProxyUser()
|
||||
val password = AppSettings.getProxyPassword()
|
||||
val credential = Credentials.basic(login, password)
|
||||
return response.request.newBuilder()
|
||||
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
val login = AppSettings.getProxyUser()
|
||||
val password = AppSettings.getProxyPassword()
|
||||
return PasswordAuthentication(login, password.toCharArray())
|
||||
}
|
||||
|
||||
private fun isProxyEnabled() = AppSettings.getProxyType() != Proxy.Type.DIRECT.ordinal
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
package org.xtimms.shirizu.core.ui.dialogs
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SignalCellularConnectedNoInternet4Bar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.components.BottomButtonShape
|
||||
import org.xtimms.shirizu.core.components.MiddleButtonShape
|
||||
import org.xtimms.shirizu.core.components.ShirizuDialogButtonVariant
|
||||
import org.xtimms.shirizu.core.components.ShirizuDialogVariant
|
||||
import org.xtimms.shirizu.core.components.TopButtonShape
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun MeteredNetworkDialog(
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onAllowOnceConfirm: () -> Unit = {},
|
||||
onAllowAlwaysConfirm: () -> Unit = {},
|
||||
) {
|
||||
ShirizuDialogVariant(
|
||||
onDismissRequest = onDismissRequest,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SignalCellularConnectedNoInternet4Bar,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = { Text(text = stringResource(id = R.string.download_with_cellular_request)) },
|
||||
buttons = {
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(id = R.string.allow_always),
|
||||
shape = TopButtonShape
|
||||
) {
|
||||
onAllowAlwaysConfirm()
|
||||
}
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(id = R.string.allow_once),
|
||||
shape = MiddleButtonShape
|
||||
) {
|
||||
onAllowOnceConfirm()
|
||||
}
|
||||
ShirizuDialogButtonVariant(
|
||||
text = stringResource(id = R.string.dont_allow),
|
||||
shape = BottomButtonShape
|
||||
) {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package org.xtimms.shirizu.core.ui.dialogs
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NotificationsActive
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import org.xtimms.shirizu.R
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun NotificationPermissionDialog(
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onPermissionGranted: () -> Unit = {}
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NotificationsActive,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.enable_notifications_desc))
|
||||
},
|
||||
title = { Text(text = stringResource(id = R.string.enable_notifications)) },
|
||||
confirmButton = {
|
||||
Button(onClick = onPermissionGranted) {
|
||||
Text(text = stringResource(id = R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
OutlinedButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = R.string.disable))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package org.xtimms.shirizu.data.repository
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.almostEquals
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.xtimms.shirizu.core.model.TagsBlacklist
|
||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.utils.lang.asArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExploreRepository @Inject constructor(
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) {
|
||||
|
||||
suspend fun findRandomManga(tagsLimit: Int): Manga {
|
||||
val tagsBlacklist = TagsBlacklist(setOf(), 0.4f)
|
||||
val tags = historyRepository.getPopularTags(tagsLimit).mapNotNull {
|
||||
if (it in tagsBlacklist) null else it.title
|
||||
}
|
||||
val sources = sourcesRepository.getEnabledSources()
|
||||
check(sources.isNotEmpty()) { "No sources available" }
|
||||
for (i in 0..4) {
|
||||
val list = getList(sources.random(), tags, tagsBlacklist)
|
||||
val manga = list.randomOrNull() ?: continue
|
||||
val details = runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
||||
}.getOrNull() ?: continue
|
||||
if ((AppSettings.isSuggestionsExcludeNsfw() && details.isNsfw) || details in tagsBlacklist) {
|
||||
continue
|
||||
}
|
||||
return details
|
||||
}
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
|
||||
suspend fun findRandomManga(source: MangaSource, tagsLimit: Int): Manga {
|
||||
val tagsBlacklist = TagsBlacklist(setOf(), 0.4f)
|
||||
val skipNsfw = AppSettings.isSuggestionsExcludeNsfw() && source.contentType != ContentType.HENTAI
|
||||
val tags = historyRepository.getPopularTags(tagsLimit).mapNotNull {
|
||||
if (it in tagsBlacklist) null else it.title
|
||||
}
|
||||
for (i in 0..4) {
|
||||
val list = getList(source, tags, tagsBlacklist)
|
||||
val manga = list.randomOrNull() ?: continue
|
||||
val details = runCatchingCancellable {
|
||||
mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
||||
}.getOrNull() ?: continue
|
||||
if ((skipNsfw && details.isNsfw) || details in tagsBlacklist) {
|
||||
continue
|
||||
}
|
||||
return details
|
||||
}
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
|
||||
private suspend fun getList(
|
||||
source: MangaSource,
|
||||
tags: List<String>,
|
||||
blacklist: TagsBlacklist,
|
||||
): List<Manga> = runCatchingCancellable {
|
||||
val repository = mangaRepositoryFactory.create(source)
|
||||
val order = repository.sortOrders.random()
|
||||
val availableTags = repository.getTags()
|
||||
val tag = tags.firstNotNullOfOrNull { title ->
|
||||
availableTags.find { x -> x.title.almostEquals(title, 0.4f) }
|
||||
}
|
||||
val list = repository.getList(
|
||||
offset = 0,
|
||||
filter = MangaListFilter.Advanced.Builder(order)
|
||||
.tags(setOfNotNull(tag))
|
||||
.build(),
|
||||
).asArrayList()
|
||||
if (AppSettings.isSuggestionsExcludeNsfw()) {
|
||||
list.removeAll { it.isNsfw }
|
||||
}
|
||||
if (blacklist.isNotEmpty()) {
|
||||
list.removeAll { manga -> manga in blacklist }
|
||||
}
|
||||
list.shuffle()
|
||||
list
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrDefault(emptyList())
|
||||
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
package org.xtimms.shirizu.data.repository
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||
import org.xtimms.shirizu.core.database.entity.toManga
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.sections.stats.domain.StatsPeriod
|
||||
import org.xtimms.shirizu.sections.stats.domain.StatsRecord
|
||||
import java.util.NavigableMap
|
||||
import java.util.TreeMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class StatsRepository @Inject constructor(
|
||||
private val db: ShirizuDatabase,
|
||||
) {
|
||||
|
||||
suspend fun getReadingStats(period: StatsPeriod, categories: Set<Long>): List<StatsRecord> {
|
||||
val fromDate = if (period == StatsPeriod.ALL) {
|
||||
0L
|
||||
} else {
|
||||
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
|
||||
}
|
||||
val stats = db.getStatsDao().getDurationStats(fromDate, null, categories)
|
||||
val result = ArrayList<StatsRecord>(stats.size)
|
||||
var other = StatsRecord(null, 0)
|
||||
val total = stats.values.sum()
|
||||
for ((mangaEntity, duration) in stats) {
|
||||
val manga = mangaEntity.toManga(emptySet())
|
||||
val percent = duration.toDouble() / total
|
||||
if (percent < 0.05) {
|
||||
other = other.copy(duration = other.duration + duration)
|
||||
} else {
|
||||
result += StatsRecord(
|
||||
manga = manga,
|
||||
duration = duration,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (other.duration != 0L) {
|
||||
result += other
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getTimePerPage(mangaId: Long): Long = db.withTransaction {
|
||||
val dao = db.getStatsDao()
|
||||
val pages = dao.getReadPagesCount(mangaId)
|
||||
val time = if (pages >= 10) {
|
||||
dao.getAverageTimePerPage(mangaId)
|
||||
} else {
|
||||
dao.getAverageTimePerPage()
|
||||
}
|
||||
time
|
||||
}
|
||||
|
||||
suspend fun getTotalPagesRead(mangaId: Long): Int {
|
||||
return db.getStatsDao().getReadPagesCount(mangaId)
|
||||
}
|
||||
|
||||
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
|
||||
val entities = db.getStatsDao().findAll(mangaId)
|
||||
val map = TreeMap<Long, Int>()
|
||||
for (e in entities) {
|
||||
map[e.startedAt] = e.pages
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
suspend fun clearStats() {
|
||||
db.getStatsDao().clear()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observeHasStats(mangaId: Long): Flow<Boolean> =
|
||||
MutableStateFlow(AppSettings.isStatisticsEnabled())
|
||||
.asStateFlow()
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
db.getStatsDao().observeRowCount(mangaId).map { it > 0 }
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@ -1,172 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.details
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Cancel
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.DownloadDone
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.components.DismissButton
|
||||
import org.xtimms.shirizu.core.components.DrawerSheetSubtitle
|
||||
import org.xtimms.shirizu.core.components.FilledButtonWithIcon
|
||||
import org.xtimms.shirizu.core.components.OutlinedButtonWithIcon
|
||||
import org.xtimms.shirizu.core.components.ShirizuModalBottomSheet
|
||||
import org.xtimms.shirizu.core.components.SingleChoiceChip
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
)
|
||||
@Composable
|
||||
fun DownloadSettingDialog(
|
||||
useDialog: Boolean = false,
|
||||
showDialog: Boolean = false,
|
||||
sheetState: SheetState,
|
||||
onDownloadConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
|
||||
val downloadButtonCallback = {
|
||||
onDismissRequest()
|
||||
onDownloadConfirm()
|
||||
}
|
||||
|
||||
val sheetContent: @Composable () -> Unit = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_before_download_text),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
DrawerSheetSubtitle(text = stringResource(id = R.string.download_format))
|
||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
|
||||
SingleChoiceChip(
|
||||
selected = true,
|
||||
onClick = {
|
||||
|
||||
},
|
||||
label = stringResource(id = R.string.auto)
|
||||
)
|
||||
SingleChoiceChip(
|
||||
selected = false,
|
||||
onClick = {
|
||||
|
||||
},
|
||||
label = stringResource(id = R.string.single_cbz)
|
||||
)
|
||||
SingleChoiceChip(
|
||||
selected = false,
|
||||
onClick = {
|
||||
|
||||
},
|
||||
label = stringResource(id = R.string.multiple_cbz)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
if (!useDialog) {
|
||||
ShirizuModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
horizontalPadding = PaddingValues(horizontal = 20.dp),
|
||||
onDismissRequest = onDismissRequest,
|
||||
content = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Icon(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
imageVector = Icons.Outlined.DoneAll,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.settings_before_download),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 16.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
sheetContent()
|
||||
val state = rememberLazyListState()
|
||||
LaunchedEffect(sheetState.isVisible) {
|
||||
state.scrollToItem(0)
|
||||
}
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
state = state,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
item {
|
||||
OutlinedButtonWithIcon(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
onClick = onDismissRequest,
|
||||
icon = Icons.Outlined.Cancel,
|
||||
text = stringResource(R.string.cancel)
|
||||
)
|
||||
}
|
||||
item {
|
||||
FilledButtonWithIcon(
|
||||
onClick = downloadButtonCallback,
|
||||
icon = Icons.Outlined.DownloadDone,
|
||||
text = stringResource(R.string.start_download),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
AlertDialog(onDismissRequest = onDismissRequest, confirmButton = {
|
||||
TextButton(onClick = downloadButtonCallback) {
|
||||
Text(text = stringResource(R.string.start_download))
|
||||
}
|
||||
}, dismissButton = { DismissButton { onDismissRequest() } }, icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DoneAll, contentDescription = null
|
||||
)
|
||||
}, title = {
|
||||
Text(
|
||||
stringResource(R.string.settings_before_download),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}, text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
sheetContent()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,510 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.settings.network
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
import androidx.compose.material.icons.outlined.PhotoSizeSelectSmall
|
||||
import androidx.compose.material.icons.outlined.SignalCellular4Bar
|
||||
import androidx.compose.material.icons.outlined.SignalCellularConnectedNoInternet4Bar
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.VpnLock
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.components.DialogSingleChoiceItem
|
||||
import org.xtimms.shirizu.core.components.PreferenceItem
|
||||
import org.xtimms.shirizu.core.components.PreferenceSubtitle
|
||||
import org.xtimms.shirizu.core.components.PreferenceSwitch
|
||||
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
||||
import org.xtimms.shirizu.core.components.ShirizuDialog
|
||||
import org.xtimms.shirizu.core.components.icons.ArrowDecisionOutline
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.getInt
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.getString
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.getValue
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.updateInt
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.updateString
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.updateValue
|
||||
import org.xtimms.shirizu.core.prefs.CELLULAR_DOWNLOAD
|
||||
import org.xtimms.shirizu.core.prefs.DOH
|
||||
import org.xtimms.shirizu.core.prefs.PROXY_ADDRESS
|
||||
import org.xtimms.shirizu.core.prefs.PROXY_PASSWORD
|
||||
import org.xtimms.shirizu.core.prefs.PROXY_PORT
|
||||
import org.xtimms.shirizu.core.prefs.PROXY_TYPE
|
||||
import org.xtimms.shirizu.core.prefs.PROXY_USER
|
||||
import org.xtimms.shirizu.core.prefs.PreferenceStrings
|
||||
import org.xtimms.shirizu.core.prefs.SSL_BYPASS
|
||||
import org.xtimms.shirizu.core.prefs.WSRV
|
||||
import org.xtimms.shirizu.utils.MaskVisualTransformation
|
||||
import org.xtimms.shirizu.utils.NumberDefaults.INPUT_LENGTH
|
||||
import org.xtimms.shirizu.utils.NumberDefaults.MASK
|
||||
import org.xtimms.shirizu.utils.NumberDefaults.MAX_PORT
|
||||
import org.xtimms.shirizu.utils.lang.intState
|
||||
import java.net.Proxy
|
||||
|
||||
const val NETWORK_DESTINATION = "network"
|
||||
|
||||
@Composable
|
||||
fun NetworkView(
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
|
||||
var showDOHDialog by remember { mutableStateOf(false) }
|
||||
var showProxyDialog by remember { mutableStateOf(false) }
|
||||
var showProxyAddressDialog by remember { mutableStateOf(false) }
|
||||
var showProxyPortDialog by remember { mutableStateOf(false) }
|
||||
var showProxyUsernameDialog by remember { mutableStateOf(false) }
|
||||
var showProxyPasswordDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var doh by DOH.intState
|
||||
var proxy by PROXY_TYPE.intState
|
||||
var address by remember(showProxyAddressDialog) { mutableStateOf(PROXY_ADDRESS.getString()) }
|
||||
var port by remember(showProxyPortDialog) { mutableIntStateOf(PROXY_PORT.getInt()) }
|
||||
var username by remember(showProxyUsernameDialog) { mutableStateOf(PROXY_USER.getString()) }
|
||||
var password by remember(showProxyPasswordDialog) { mutableStateOf(PROXY_PASSWORD.getString()) }
|
||||
|
||||
var isSSLBypassEnabled by remember {
|
||||
mutableStateOf(AppSettings.isSSLBypassEnabled())
|
||||
}
|
||||
|
||||
var isImageOptimizationEnabled by remember {
|
||||
mutableStateOf(AppSettings.isImagesProxyEnabled())
|
||||
}
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.network),
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
)
|
||||
) {
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.general))
|
||||
}
|
||||
item {
|
||||
var isDownloadWithCellularEnabled by remember {
|
||||
mutableStateOf(getValue(CELLULAR_DOWNLOAD))
|
||||
}
|
||||
PreferenceSwitch(
|
||||
title = stringResource(R.string.download_with_cellular),
|
||||
description = stringResource(R.string.download_with_cellular_desc),
|
||||
icon = if (isDownloadWithCellularEnabled) Icons.Outlined.SignalCellular4Bar
|
||||
else Icons.Outlined.SignalCellularConnectedNoInternet4Bar,
|
||||
isChecked = isDownloadWithCellularEnabled,
|
||||
onClick = {
|
||||
isDownloadWithCellularEnabled = !isDownloadWithCellularEnabled
|
||||
updateValue(
|
||||
CELLULAR_DOWNLOAD,
|
||||
isDownloadWithCellularEnabled
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.advanced))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
title = stringResource(id = R.string.dns_over_https),
|
||||
description = PreferenceStrings.getDOHDescRes(doh),
|
||||
icon = Icons.Outlined.Dns
|
||||
) { showDOHDialog = true }
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = R.string.images_optimization_proxy),
|
||||
description = stringResource(id = R.string.images_optimization_proxy_desc),
|
||||
icon = Icons.Outlined.PhotoSizeSelectSmall,
|
||||
isChecked = isImageOptimizationEnabled,
|
||||
) {
|
||||
isImageOptimizationEnabled = !isImageOptimizationEnabled
|
||||
updateValue(WSRV, isImageOptimizationEnabled)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = R.string.ignore_ssl_errors),
|
||||
description = stringResource(id = R.string.ignore_ssl_errors_desc),
|
||||
icon = Icons.Outlined.VpnLock,
|
||||
isChecked = isSSLBypassEnabled,
|
||||
) {
|
||||
isSSLBypassEnabled = !isSSLBypassEnabled
|
||||
updateValue(SSL_BYPASS, isSSLBypassEnabled)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.proxy))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
title = stringResource(id = R.string.proxy_type),
|
||||
description = PreferenceStrings.getProxyDescRes(proxy),
|
||||
) { showProxyDialog = true }
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
enabled = proxy != Proxy.Type.DIRECT.ordinal,
|
||||
title = stringResource(id = R.string.proxy_address),
|
||||
description = address,
|
||||
) { showProxyAddressDialog = true }
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
enabled = proxy != Proxy.Type.DIRECT.ordinal,
|
||||
title = stringResource(id = R.string.proxy_port),
|
||||
description = port.toString()
|
||||
) { showProxyPortDialog = true }
|
||||
}
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.proxy_authorization))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
enabled = proxy != Proxy.Type.DIRECT.ordinal,
|
||||
title = stringResource(id = R.string.proxy_username),
|
||||
description = username,
|
||||
) { showProxyUsernameDialog = true }
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
enabled = proxy != Proxy.Type.DIRECT.ordinal,
|
||||
title = stringResource(id = R.string.proxy_password),
|
||||
description = String(CharArray(password.length) { '\u2022' }),
|
||||
) { showProxyPasswordDialog = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showDOHDialog) {
|
||||
DOHSettingDialog(provider = doh,
|
||||
onDismissRequest = { showDOHDialog = false }) {
|
||||
doh = it
|
||||
DOH.updateInt(it)
|
||||
}
|
||||
}
|
||||
if (showProxyDialog) {
|
||||
ProxySettingDialog(type = proxy,
|
||||
onDismissRequest = { showProxyDialog = false }) {
|
||||
proxy = it
|
||||
PROXY_TYPE.updateInt(it)
|
||||
}
|
||||
}
|
||||
if (showProxyAddressDialog) {
|
||||
ProxyAddressSettingDialog(address = address,
|
||||
onDismissRequest = { showProxyAddressDialog = false }) {
|
||||
address = it
|
||||
PROXY_ADDRESS.updateString(it)
|
||||
}
|
||||
}
|
||||
if (showProxyPortDialog) {
|
||||
ProxyPortSettingDialog(port = port.toString(),
|
||||
onDismissRequest = { showProxyPortDialog = false }) {
|
||||
port = it
|
||||
PROXY_PORT.updateInt(it)
|
||||
}
|
||||
}
|
||||
if (showProxyUsernameDialog) {
|
||||
ProxyUsernameSettingDialog(username = username,
|
||||
onDismissRequest = { showProxyUsernameDialog = false }) {
|
||||
username = it
|
||||
PROXY_USER.updateString(it)
|
||||
}
|
||||
}
|
||||
if (showProxyPasswordDialog) {
|
||||
ProxyPasswordSettingDialog(password = password,
|
||||
onDismissRequest = { showProxyPasswordDialog = false }) {
|
||||
password = it
|
||||
PROXY_PASSWORD.updateString(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DOHSettingDialog(
|
||||
provider: Int = 0,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (Int) -> Unit = {}
|
||||
) {
|
||||
var dohProvider by remember { mutableIntStateOf(provider) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.Dns, null) },
|
||||
title = {
|
||||
Text(stringResource(R.string.dns_over_https))
|
||||
}, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(dohProvider)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
}, text = {
|
||||
Column {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.doh_desc),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
LazyColumn {
|
||||
for (i in 0..3) {
|
||||
item {
|
||||
DialogSingleChoiceItem(
|
||||
text = PreferenceStrings.getDOHDescRes(i),
|
||||
selected = dohProvider == i
|
||||
) {
|
||||
dohProvider = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProxySettingDialog(
|
||||
type: Int = 0,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (Int) -> Unit = {}
|
||||
) {
|
||||
var proxy by remember { mutableIntStateOf(type) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.ArrowDecisionOutline, null) },
|
||||
title = {
|
||||
Text(stringResource(R.string.proxy_type))
|
||||
}, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(proxy)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
}, text = {
|
||||
Column {
|
||||
LazyColumn {
|
||||
for (i in 0..2) {
|
||||
item {
|
||||
DialogSingleChoiceItem(
|
||||
text = PreferenceStrings.getProxyDescRes(i),
|
||||
selected = proxy == i
|
||||
) {
|
||||
proxy = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProxyAddressSettingDialog(
|
||||
address: String = "",
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {}
|
||||
) {
|
||||
var proxyAddress by remember { mutableStateOf(address) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.proxy_address))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(proxyAddress)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
value = proxyAddress,
|
||||
onValueChange = { proxyAddress = it },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProxyPortSettingDialog(
|
||||
port: String = "",
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (Int) -> Unit = {}
|
||||
) {
|
||||
var proxyPort by remember { mutableStateOf(port) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.proxy_port))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(enabled = proxyPort.toInt() < MAX_PORT,
|
||||
onClick = {
|
||||
onConfirm(proxyPort.toInt())
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
value = proxyPort,
|
||||
isError = proxyPort.toInt() > MAX_PORT,
|
||||
onValueChange = { it ->
|
||||
if (it.length <= INPUT_LENGTH) {
|
||||
proxyPort = it.filter { it.isDigit() }
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Decimal
|
||||
),
|
||||
visualTransformation = MaskVisualTransformation(MASK)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProxyUsernameSettingDialog(
|
||||
username: String = "",
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {}
|
||||
) {
|
||||
var proxyUsername by remember { mutableStateOf(username) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.proxy_username))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(proxyUsername)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
value = proxyUsername,
|
||||
onValueChange = { proxyUsername = it },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProxyPasswordSettingDialog(
|
||||
password: String = "",
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirm: (String) -> Unit = {}
|
||||
) {
|
||||
var proxyPassword by remember { mutableStateOf(password) }
|
||||
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
ShirizuDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.proxy_password))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(proxyPassword)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
value = proxyPassword,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
onValueChange = { proxyPassword = it },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Password
|
||||
),
|
||||
trailingIcon = {
|
||||
val image =
|
||||
if (passwordVisible) Icons.Outlined.Visibility else Icons.Outlined.VisibilityOff
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(imageVector = image, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.settings.services
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.QueryStats
|
||||
import androidx.compose.material.icons.outlined.Timelapse
|
||||
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.res.stringResource
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.components.PreferenceSubtitle
|
||||
import org.xtimms.shirizu.core.components.PreferenceSwitch
|
||||
import org.xtimms.shirizu.core.components.PreferenceSwitchWithDivider
|
||||
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
||||
import org.xtimms.shirizu.core.components.icons.Creation
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.core.prefs.READING_TIME
|
||||
import org.xtimms.shirizu.core.prefs.RELATED
|
||||
import org.xtimms.shirizu.core.prefs.STATISTICS
|
||||
import org.xtimms.shirizu.core.prefs.SUGGESTIONS
|
||||
|
||||
const val SERVICES_DESTINATION = "services"
|
||||
|
||||
@Composable
|
||||
fun ServicesView(
|
||||
navigateBack: () -> Unit,
|
||||
navigateToSuggestionsSettings: () -> Unit,
|
||||
navigateToStatistics: () -> Unit
|
||||
) {
|
||||
|
||||
var isSuggestionsEnabled by remember { mutableStateOf(AppSettings.isSuggestionsEnabled()) }
|
||||
var isRelatedEnabled by remember { mutableStateOf(AppSettings.isRelatedMangaEnabled()) }
|
||||
var isStatisticsEnabled by remember { mutableStateOf(AppSettings.isStatisticsEnabled()) }
|
||||
var isReadingTimeEstimationEnabled by remember { mutableStateOf(AppSettings.isReadingTimeEstimationEnabled()) }
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.services),
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
)
|
||||
) {
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.manga))
|
||||
}
|
||||
item {
|
||||
PreferenceSwitchWithDivider(
|
||||
title = stringResource(R.string.suggestions),
|
||||
description = stringResource(id = R.string.suggestions_summary),
|
||||
icon = Icons.Outlined.Creation,
|
||||
isChecked = isSuggestionsEnabled,
|
||||
onClick = navigateToSuggestionsSettings,
|
||||
onChecked = {
|
||||
isSuggestionsEnabled = !isSuggestionsEnabled
|
||||
AppSettings.updateValue(SUGGESTIONS, isSuggestionsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = R.string.related_manga),
|
||||
description = stringResource(id = R.string.related_manga_summary),
|
||||
icon = Icons.Outlined.CollectionsBookmark,
|
||||
isChecked = isRelatedEnabled,
|
||||
onClick = {
|
||||
isRelatedEnabled = !isRelatedEnabled
|
||||
AppSettings.updateValue(RELATED, isRelatedEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.statistics))
|
||||
}
|
||||
item {
|
||||
PreferenceSwitchWithDivider(
|
||||
title = stringResource(R.string.recording_statistics),
|
||||
description = if (isStatisticsEnabled) stringResource(id = R.string.enabled) else stringResource(
|
||||
id = R.string.disabled
|
||||
),
|
||||
icon = Icons.Outlined.QueryStats,
|
||||
isChecked = isStatisticsEnabled,
|
||||
onClick = navigateToStatistics,
|
||||
onChecked = {
|
||||
isStatisticsEnabled = !isStatisticsEnabled
|
||||
AppSettings.updateValue(STATISTICS, isStatisticsEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = R.string.show_estimated_read_time),
|
||||
description = stringResource(id = R.string.show_estimated_read_time_desc),
|
||||
icon = Icons.Outlined.Timelapse,
|
||||
isChecked = isReadingTimeEstimationEnabled,
|
||||
onClick = {
|
||||
isReadingTimeEstimationEnabled = !isReadingTimeEstimationEnabled
|
||||
AppSettings.updateValue(READING_TIME, isReadingTimeEstimationEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.settings.services.suggestions
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.NoAdultContent
|
||||
import androidx.compose.material.icons.outlined.Notifications
|
||||
import androidx.compose.material.icons.outlined.Wifi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.components.PreferenceInfo
|
||||
import org.xtimms.shirizu.core.components.PreferenceItem
|
||||
import org.xtimms.shirizu.core.components.PreferenceSubtitle
|
||||
import org.xtimms.shirizu.core.components.PreferenceSwitch
|
||||
import org.xtimms.shirizu.core.components.PreferenceSwitchWithContainer
|
||||
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings.updateBoolean
|
||||
import org.xtimms.shirizu.core.prefs.SUGGESTIONS
|
||||
import org.xtimms.shirizu.core.prefs.SUGGESTIONS_NONMETERED
|
||||
import org.xtimms.shirizu.core.prefs.SUGGESTIONS_NOTIFICATIONS
|
||||
import org.xtimms.shirizu.core.prefs.SUGGESTIONS_NSFW
|
||||
import org.xtimms.shirizu.utils.lang.booleanState
|
||||
import org.xtimms.shirizu.utils.system.toast
|
||||
|
||||
const val SUGGESTIONS_SETTINGS_DESTINATION = "suggestions_settings"
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun SuggestionsSettingsView(
|
||||
navigateBack: () -> Unit
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
var suggestionsEnabled by SUGGESTIONS.booleanState
|
||||
var nonMeteredNetwork by SUGGESTIONS_NONMETERED.booleanState
|
||||
var notifications by SUGGESTIONS_NOTIFICATIONS.booleanState
|
||||
var nsfwSuggestions by SUGGESTIONS_NSFW.booleanState
|
||||
|
||||
val enableSuggestionsNotifications = {
|
||||
notifications = !notifications
|
||||
SUGGESTIONS_NOTIFICATIONS.updateBoolean(notifications)
|
||||
}
|
||||
|
||||
val notificationPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(
|
||||
permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
) { b: Boolean ->
|
||||
if (b) {
|
||||
enableSuggestionsNotifications()
|
||||
} else {
|
||||
context.toast(R.string.permission_denied)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < TIRAMISU")
|
||||
}
|
||||
|
||||
val checkPermission = {
|
||||
if (notificationPermission.status == PermissionStatus.Granted) {
|
||||
enableSuggestionsNotifications()
|
||||
} else {
|
||||
notificationPermission.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
ScaffoldWithTopAppBar(
|
||||
title = stringResource(R.string.suggestions),
|
||||
navigateBack = navigateBack
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
)
|
||||
) {
|
||||
item {
|
||||
PreferenceSwitchWithContainer(
|
||||
title = stringResource(id = R.string.enable_suggestions),
|
||||
isChecked = suggestionsEnabled
|
||||
) {
|
||||
suggestionsEnabled = !suggestionsEnabled
|
||||
SUGGESTIONS.updateBoolean(suggestionsEnabled)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
enabled = suggestionsEnabled,
|
||||
icon = Icons.Outlined.Wifi,
|
||||
title = stringResource(id = R.string.only_on_wifi),
|
||||
description = stringResource(id = R.string.only_on_wifi_desc),
|
||||
isChecked = nonMeteredNetwork
|
||||
) {
|
||||
nonMeteredNetwork = !nonMeteredNetwork
|
||||
SUGGESTIONS_NONMETERED.updateBoolean(nonMeteredNetwork)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
enabled = suggestionsEnabled,
|
||||
icon = Icons.Outlined.Notifications,
|
||||
title = stringResource(id = R.string.suggestions_notifications),
|
||||
description = stringResource(id = R.string.suggestions_notifications_desc)
|
||||
) {
|
||||
checkPermission()
|
||||
}
|
||||
}
|
||||
item{
|
||||
PreferenceSubtitle(text = stringResource(id = R.string.advanced))
|
||||
}
|
||||
item {
|
||||
PreferenceItem(
|
||||
enabled = suggestionsEnabled,
|
||||
title = stringResource(id = R.string.exclude_genres),
|
||||
description = stringResource(id = R.string.exclude_genres_desc),
|
||||
icon = Icons.Outlined.FilterAlt
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceSwitch(
|
||||
enabled = suggestionsEnabled,
|
||||
title = stringResource(id = R.string.do_not_suggest_nsfw_manga),
|
||||
icon = Icons.Outlined.NoAdultContent,
|
||||
isChecked = nsfwSuggestions
|
||||
) {
|
||||
nsfwSuggestions = !nsfwSuggestions
|
||||
SUGGESTIONS_NSFW.updateBoolean(nsfwSuggestions)
|
||||
}
|
||||
}
|
||||
item {
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
PreferenceInfo(text = stringResource(id = R.string.suggestions_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.stats
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.take
|
||||
import org.xtimms.shirizu.R
|
||||
import org.xtimms.shirizu.core.base.viewmodel.KotatsuBaseViewModel
|
||||
import org.xtimms.shirizu.core.model.FavouriteCategory
|
||||
import org.xtimms.shirizu.data.repository.FavouritesRepository
|
||||
import org.xtimms.shirizu.data.repository.StatsRepository
|
||||
import org.xtimms.shirizu.sections.stats.domain.StatsPeriod
|
||||
import org.xtimms.shirizu.sections.stats.domain.StatsRecord
|
||||
import org.xtimms.shirizu.utils.ReversibleAction
|
||||
import org.xtimms.shirizu.utils.lang.MutableEventFlow
|
||||
import org.xtimms.shirizu.utils.lang.call
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StatsViewModel @Inject constructor(
|
||||
private val repository: StatsRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : KotatsuBaseViewModel() {
|
||||
|
||||
val period = MutableStateFlow(StatsPeriod.WEEK)
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val selectedCategories = MutableStateFlow<Set<Long>>(emptySet())
|
||||
val favoriteCategories = favouritesRepository.observeCategories()
|
||||
.take(1)
|
||||
|
||||
val readingStats = MutableStateFlow<List<StatsRecord>>(emptyList())
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
combine<StatsPeriod, Set<Long>, Pair<StatsPeriod, Set<Long>>>(
|
||||
period,
|
||||
selectedCategories,
|
||||
::Pair,
|
||||
).collectLatest { p ->
|
||||
readingStats.value = withLoading {
|
||||
repository.getReadingStats(p.first, p.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCategoryChecked(category: FavouriteCategory, checked: Boolean) {
|
||||
val snapshot = selectedCategories.value.toMutableSet()
|
||||
if (checked) {
|
||||
snapshot.add(category.id)
|
||||
} else {
|
||||
snapshot.remove(category.id)
|
||||
}
|
||||
selectedCategories.value = snapshot
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
repository.clearStats()
|
||||
readingStats.value = emptyList()
|
||||
onActionDone.call(ReversibleAction(R.string.stats_cleared, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.stats.domain
|
||||
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||
import org.xtimms.shirizu.core.database.entity.StatsEntity
|
||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||
import org.xtimms.shirizu.sections.reader.ReaderState
|
||||
import org.xtimms.shirizu.utils.RetainedLifecycleCoroutineScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
class StatsCollector @Inject constructor(
|
||||
private val db: ShirizuDatabase,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
) {
|
||||
|
||||
private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle)
|
||||
private val stats = LongSparseArray<Entry>(1)
|
||||
|
||||
@Synchronized
|
||||
fun onStateChanged(mangaId: Long, state: ReaderState) {
|
||||
if (!AppSettings.isStatisticsEnabled()) {
|
||||
return
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val entry = stats[mangaId]
|
||||
if (entry == null) {
|
||||
stats[mangaId] = Entry(
|
||||
state = state,
|
||||
stats = StatsEntity(
|
||||
mangaId = mangaId,
|
||||
startedAt = now,
|
||||
duration = 0,
|
||||
pages = 0,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
val pagesDelta = if (entry.state.page != state.page || entry.state.chapterId != state.chapterId) 1 else 0
|
||||
val newEntry = entry.copy(
|
||||
stats = StatsEntity(
|
||||
mangaId = mangaId,
|
||||
startedAt = entry.stats.startedAt,
|
||||
duration = now - entry.stats.startedAt,
|
||||
pages = entry.stats.pages + pagesDelta,
|
||||
),
|
||||
)
|
||||
stats[mangaId] = newEntry
|
||||
commit(newEntry.stats)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun onPause(mangaId: Long) {
|
||||
stats.remove(mangaId)
|
||||
}
|
||||
|
||||
private fun commit(entity: StatsEntity) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
db.getStatsDao().upsert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Entry(
|
||||
val state: ReaderState,
|
||||
val stats: StatsEntity,
|
||||
)
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.stats.domain
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.xtimms.shirizu.R
|
||||
|
||||
enum class StatsPeriod(
|
||||
@StringRes val titleResId: Int,
|
||||
val days: Int,
|
||||
) {
|
||||
|
||||
DAY(R.string.day, 1),
|
||||
WEEK(R.string.week, 7),
|
||||
MONTH(R.string.month, 30),
|
||||
MONTHS_3(R.string.three_months, 90),
|
||||
ALL(R.string.all_time, Int.MAX_VALUE),
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package org.xtimms.shirizu.sections.stats.domain
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.xtimms.shirizu.core.model.ListModel
|
||||
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class StatsRecord(
|
||||
val manga: Manga?,
|
||||
val duration: Long,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is StatsRecord && other.manga == manga
|
||||
}
|
||||
|
||||
val time: ReadingTime
|
||||
|
||||
init {
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(duration).toInt()
|
||||
time = ReadingTime(
|
||||
minutes = minutes % 60,
|
||||
hours = minutes / 60,
|
||||
isContinue = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package org.xtimms.shirizu.utils
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
|
||||
|
||||
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
var out = ""
|
||||
var maskIndex = 0
|
||||
text.forEach { char ->
|
||||
while (specialSymbolsIndices.contains(maskIndex)) {
|
||||
out += mask[maskIndex]
|
||||
maskIndex++
|
||||
}
|
||||
out += char
|
||||
maskIndex++
|
||||
}
|
||||
return TransformedText(AnnotatedString(out), offsetTranslator())
|
||||
}
|
||||
|
||||
private fun offsetTranslator() = object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
val offsetValue = offset.absoluteValue
|
||||
if (offsetValue == 0) return 0
|
||||
var numberOfHashtags = 0
|
||||
val masked = mask.takeWhile {
|
||||
if (it == '#') numberOfHashtags++
|
||||
numberOfHashtags < offsetValue
|
||||
}
|
||||
return masked.length + 1
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return mask.take(offset.absoluteValue).count { it == '#' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NumberDefaults {
|
||||
const val MASK = "#####"
|
||||
const val INPUT_LENGTH = 5
|
||||
const val MAX_PORT = 65535
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 708 B |
Binary file not shown.
|
Before Width: | Height: | Size: 469 B |
Binary file not shown.
|
Before Width: | Height: | Size: 955 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
@ -1 +0,0 @@
|
||||
/build
|
||||
@ -1,52 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.test")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.shirizu.benchmark"
|
||||
compileSdk = 34
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// This benchmark buildType is used for benchmarking, and should function like your
|
||||
// release build (for example, with minification on). It"s signed with a debug key
|
||||
// for easy local/CI testing.
|
||||
create("benchmark") {
|
||||
isDebuggable = true
|
||||
signingConfig = getByName("debug").signingConfig
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":android-app:app"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.test.ext:junit:1.1.5")
|
||||
implementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation("androidx.test.uiautomator:uiautomator:2.3.0")
|
||||
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.3")
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants(selector().all()) {
|
||||
it.enable = it.buildType == "benchmark"
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<manifest />
|
||||
@ -1,24 +0,0 @@
|
||||
package app.shirizu.benchmark
|
||||
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.uiautomator.By
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class BaselineProfileGenerator {
|
||||
|
||||
@get:Rule
|
||||
val rule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun generate() = rule.collect(
|
||||
packageName = "org.xtimms.shirizu.benchmark",
|
||||
profileBlock = {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
device.findObject(By.text("Shelf")).click()
|
||||
device.findObject(By.text("History")).click()
|
||||
device.findObject(By.text("Explore")).click()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
package app.shirizu.benchmark
|
||||
|
||||
import androidx.benchmark.macro.BaselineProfileMode
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a cold state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class ColdStartupBenchmark : AbstractStartupBenchmark(StartupMode.COLD)
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a warm state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class WarmStartupBenchmark : AbstractStartupBenchmark(StartupMode.WARM)
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a hot state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class HotStartupBenchmark : AbstractStartupBenchmark(StartupMode.HOT)
|
||||
|
||||
/**
|
||||
* Base class for benchmarks with different startup modes.
|
||||
* Enables app startups from various states of baseline profile or [CompilationMode]s.
|
||||
*/
|
||||
abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun startupNoCompilation() = startup(CompilationMode.None())
|
||||
|
||||
@Test
|
||||
fun startupBaselineProfileDisabled() = startup(
|
||||
CompilationMode.Partial(
|
||||
baselineProfileMode = BaselineProfileMode.Disable,
|
||||
warmupIterations = 1,
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun startupBaselineProfile() = startup(
|
||||
CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun startupFullCompilation() = startup(CompilationMode.Full())
|
||||
|
||||
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
|
||||
packageName = "org.xtimms.shirizu.benchmark",
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
iterations = 10,
|
||||
startupMode = startupMode,
|
||||
setupBlock = {
|
||||
pressHome()
|
||||
},
|
||||
) {
|
||||
startActivityAndWait()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package org.xtimms.shirizu.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.shirizu.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(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package org.xtimms.shirizu.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 ShirizuDialog(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue