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