Compare commits

..

No commits in common. '774bb84f7f3e7a3b7b65aeb7f27fbe6b58fd330d' and '746934d421a1ddbfcd9d65b47f5fc1cd9c749aac' have entirely different histories.

@ -16,32 +16,6 @@
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="83150218">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_5_API_34" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
<entry key="1878637654">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_5_API_34" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>

@ -2,14 +2,24 @@
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="BaselineProfileGenerator">
<entry key="AppBackupAgentTest">
<State />
</entry>
<entry key="Generate Baseline Profile">
<State />
</entry>
<entry key="android-app.app">
<State />
<entry key="app">
<State>
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\xtimms\.android\avd\Pixel_5_API_34.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-04-06T08:29:40.034958200Z" />
</State>
</entry>
</value>
</component>

@ -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,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,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()
}
}

@ -15,10 +15,8 @@ plugins {
id("dagger.hilt.android.plugin")
}
val acraAuthLogin: String =
gradleLocalProperties(rootDir).getProperty("authLogin") ?: "\"acra_login\""
val acraAuthPassword: String =
gradleLocalProperties(rootDir).getProperty("authPassword") ?: "\"acra_password\""
val acraAuthLogin: String = gradleLocalProperties(rootDir).getProperty("authLogin") ?: "\"acra_login\""
val acraAuthPassword: String = gradleLocalProperties(rootDir).getProperty("authPassword") ?: "\"acra_password\""
android {
namespace = "org.xtimms.shirizu"
@ -43,45 +41,27 @@ android {
vectorDrawables {
useSupportLibrary = true
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
"room.generateKotlin" to "true",
"room.schemaLocation" to "$projectDir/schemas"
)
}
}
}
buildTypes {
named("debug") {
versionNameSuffix = "-${getCommitCount()}"
debug {
applicationIdSuffix = ".debug"
}
named("release") {
isShrinkResources = true
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
create("benchmark") {
initWith(getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
}
}
sourceSets {
getByName("benchmark").res.srcDirs("src/debug/res")
}
flavorDimensions.add("default")
productFlavors {
create("standard") {
dimension = "default"
}
create("dev") {
dimension = "default"
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
@ -131,7 +111,6 @@ dependencies {
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("androidx.room:room-testing:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("ch.acra:acra-http:5.9.7")
implementation("com.github.solkin:disk-lru-cache:1.4")
@ -140,7 +119,6 @@ dependencies {
implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
implementation("com.google.accompanist:accompanist-pager:0.32.0")
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
implementation("com.google.dagger:hilt-android:2.51")
kapt("com.google.dagger:hilt-compiler:2.51")
implementation("androidx.hilt:hilt-work:1.2.0")
@ -171,15 +149,6 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
androidComponents {
beforeVariants { variantBuilder ->
// Disables standardBenchmark
if (variantBuilder.buildType == "benchmark") {
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
}
}
}
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
fun Project.getCommitCount(): String {

@ -27,16 +27,16 @@
android:name=".App"
android:allowBackup="false"
android:backupAgent=".sections.settings.backup.AppBackupAgent"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.Shirizu"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu">
<activity
@ -50,6 +50,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".crash.CrashActivity"
android:exported="false"
@ -69,6 +70,7 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
@ -89,6 +91,7 @@
<meta-data
android:name="com.samsung.android.icon_container.has_icon_container"
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
</application>
</manifest>

@ -4,10 +4,8 @@ import android.app.Application
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Build
import android.os.StrictMode
import androidx.core.content.getSystemService
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.WorkManager
@ -23,10 +21,9 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.xtimms.shirizu.core.database.ShirizuDatabase
import org.xtimms.shirizu.core.prefs.AppSettings
import org.xtimms.shirizu.core.prefs.KotatsuAppSettings
import org.xtimms.shirizu.core.updates.Updater
import org.xtimms.shirizu.crash.CrashActivity
import org.xtimms.shirizu.crash.GlobalExceptionHandler
import org.xtimms.shirizu.utils.lang.processLifecycleScope
import org.xtimms.shirizu.work.WorkScheduleManager
import javax.inject.Inject
@ -64,7 +61,6 @@ class App : Application(), Configuration.Provider {
) else getPackageInfo(packageName, 0)
}
DynamicColors.applyToActivitiesIfAvailable(this)
connectivityManager = getSystemService()!!
processLifecycleScope.launch(Dispatchers.IO) {
try {
@ -74,29 +70,30 @@ class App : Application(), Configuration.Provider {
}
}
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
httpSender {
uri = BuildConfig.ACRA_URI
basicAuthLogin = BuildConfig.ACRA_AUTH_LOGIN
basicAuthPassword = BuildConfig.ACRA_AUTH_PASSWORD
httpMethod = HttpSender.Method.POST
// GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
if (AppSettings.isACRAEnabled()) {
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
httpSender {
uri = BuildConfig.ACRA_URI
basicAuthLogin = BuildConfig.ACRA_AUTH_LOGIN
basicAuthPassword = BuildConfig.ACRA_AUTH_PASSWORD
httpMethod = HttpSender.Method.POST
}
reportContent = listOf(
ReportField.PACKAGE_NAME,
ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.ANDROID_VERSION,
ReportField.PHONE_MODEL,
ReportField.STACK_TRACE,
ReportField.CRASH_CONFIGURATION,
ReportField.CUSTOM_DATA,
)
}
reportContent = listOf(
ReportField.PACKAGE_NAME,
ReportField.INSTALLATION_ID,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.ANDROID_VERSION,
ReportField.PHONE_MODEL,
ReportField.STACK_TRACE,
ReportField.CRASH_CONFIGURATION,
ReportField.CUSTOM_DATA,
)
}
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
workScheduleManager.init()
}
@ -126,8 +123,8 @@ class App : Application(), Configuration.Provider {
companion object {
lateinit var packageInfo: PackageInfo
lateinit var connectivityManager: ConnectivityManager
@Suppress("DEPRECATION")
fun getVersionReport(): String {
val versionName = packageInfo.versionName
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

@ -3,12 +3,10 @@ package org.xtimms.shirizu
import android.os.Build
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
@ -31,10 +29,9 @@ val LocalSeedColor = compositionLocalOf { SEED }
val LocalDynamicColorSwitch = compositionLocalOf { false }
val LocalPaletteStyleIndex = compositionLocalOf { 0 }
val LocalWindowInsets = compositionLocalOf { PaddingValues(0.dp) }
val LocalWindowWidthState = staticCompositionLocalOf { WindowWidthSizeClass.Compact }
@Composable
fun SettingsProvider(windowWidthSizeClass: WindowWidthSizeClass, content: @Composable () -> Unit) {
fun SettingsProvider(content: @Composable () -> Unit) {
AppSettings.AppSettingsStateFlow.collectAsState().value.run {
CompositionLocalProvider(
LocalDarkTheme provides darkTheme,
@ -46,7 +43,6 @@ fun SettingsProvider(windowWidthSizeClass: WindowWidthSizeClass, content: @Compo
else Color(seedColor).toTonalPalettes(
paletteStyles.getOrElse(paletteStyleIndex) { PaletteStyle.TonalSpot }
),
LocalWindowWidthState provides windowWidthSizeClass,
LocalDynamicColorSwitch provides isDynamicColorEnabled,
content = content
)

@ -65,7 +65,7 @@ import org.xtimms.shirizu.core.components.NavigationRail
import org.xtimms.shirizu.core.components.TopAppBar
import org.xtimms.shirizu.core.logs.FileLogger
import org.xtimms.shirizu.core.prefs.AppSettings
import org.xtimms.shirizu.core.ui.dialogs.UpdateDialogImpl
import org.xtimms.shirizu.core.screens.UpdateDialogImpl
import org.xtimms.shirizu.core.updates.Updater
import org.xtimms.shirizu.ui.theme.ShirizuTheme
import org.xtimms.shirizu.utils.system.setLanguage
@ -112,7 +112,7 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController()
val windowSizeClass = calculateWindowSizeClass(this)
val isCompactScreen = LocalWindowWidthState.current == WindowWidthSizeClass.Compact
val isCompactScreen = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
val settings =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -141,7 +141,7 @@ class MainActivity : ComponentActivity() {
isReady.value = true
}
if (isReady.value) {
SettingsProvider(windowSizeClass.widthSizeClass) {
SettingsProvider {
ShirizuTheme(
darkTheme = LocalDarkTheme.current.isDarkTheme(),
isDynamicColorEnabled = LocalDynamicColorSwitch.current,

@ -66,10 +66,6 @@ import org.xtimms.shirizu.sections.settings.backup.RESTORE_DESTINATION
import org.xtimms.shirizu.sections.settings.backup.RestoreItemsView
import org.xtimms.shirizu.sections.settings.network.NETWORK_DESTINATION
import org.xtimms.shirizu.sections.settings.network.NetworkView
import org.xtimms.shirizu.sections.settings.services.SERVICES_DESTINATION
import org.xtimms.shirizu.sections.settings.services.ServicesView
import org.xtimms.shirizu.sections.settings.services.suggestions.SUGGESTIONS_SETTINGS_DESTINATION
import org.xtimms.shirizu.sections.settings.services.suggestions.SuggestionsSettingsView
import org.xtimms.shirizu.sections.settings.shelf.SHELF_SETTINGS_DESTINATION
import org.xtimms.shirizu.sections.settings.shelf.ShelfSettingsView
import org.xtimms.shirizu.sections.settings.shelf.categories.CATEGORIES_DESTINATION
@ -236,7 +232,6 @@ fun Navigation(
},
navigateToMangaSources = { navController.navigate(SOURCES_DESTINATION) },
navigateToNetwork = { navController.navigate(NETWORK_DESTINATION) },
navigateToServicesSettings = { navController.navigate(SERVICES_DESTINATION) },
navigateToShelfSettings = { navController.navigate(SHELF_SETTINGS_DESTINATION) },
navigateToStorage = { navController.navigate(STORAGE_DESTINATION) }
)
@ -313,20 +308,6 @@ fun Navigation(
)
}
composable(SERVICES_DESTINATION) {
ServicesView(
navigateBack = navigateBack,
navigateToSuggestionsSettings = { navController.navigate(SUGGESTIONS_SETTINGS_DESTINATION) },
navigateToStatistics = { navController.navigate(STATS_DESTINATION) }
)
}
composable(SUGGESTIONS_SETTINGS_DESTINATION) {
SuggestionsSettingsView(
navigateBack = navigateBack
)
}
composable(NETWORK_DESTINATION) {
NetworkView(
navigateBack = navigateBack,

@ -63,13 +63,6 @@ abstract class KotatsuBaseViewModel : ViewModel() {
loadingCounter.decrement()
}
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
loadingCounter.increment()
block()
} finally {
loadingCounter.decrement()
}
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
errorEvent.call(error)
}

@ -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()
}
}
}
}
}
}
}

@ -15,17 +15,16 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.xtimms.shirizu.ui.theme.applyOpacity
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExploreButton(
text: String,
icon: Any? = null,
icon: ImageVector,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
@ -43,29 +42,14 @@ fun ExploreButton(
.height(40.dp),
verticalAlignment = Alignment.CenterVertically
) {
when (icon) {
is ImageVector -> {
Icon(
imageVector = icon,
contentDescription = text,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
is Painter -> {
Icon(
painter = icon,
contentDescription = text,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
Icon(
imageVector = icon,
contentDescription = text,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = text,
color = MaterialTheme.colorScheme.onSurface,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save