Compare commits

..

4 Commits

@ -16,6 +16,32 @@
</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,24 +2,14 @@
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="AppBackupAgentTest">
<entry key="BaselineProfileGenerator">
<State />
</entry>
<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 key="Generate Baseline Profile">
<State />
</entry>
<entry key="android-app.app">
<State />
</entry>
</value>
</component>

@ -15,8 +15,10 @@ 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"
@ -41,27 +43,45 @@ android {
vectorDrawables {
useSupportLibrary = true
}
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
"room.generateKotlin" to "true",
"room.schemaLocation" to "$projectDir/schemas"
)
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
buildTypes {
debug {
named("debug") {
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
}
release {
named("release") {
isShrinkResources = true
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
@ -111,6 +131,7 @@ 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")
@ -119,6 +140,7 @@ 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")
@ -149,6 +171,15 @@ 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 {

@ -0,0 +1,57 @@
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"
}
}

@ -27,16 +27,16 @@
android:name=".App"
android:allowBackup="false"
android:backupAgent=".sections.settings.backup.AppBackupAgent"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
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,7 +50,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".crash.CrashActivity"
android:exported="false"
@ -70,7 +69,6 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
@ -91,7 +89,6 @@
<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>

File diff suppressed because it is too large Load Diff

@ -4,8 +4,10 @@ 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
@ -21,9 +23,10 @@ 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
@ -61,6 +64,7 @@ class App : Application(), Configuration.Provider {
) else getPackageInfo(packageName, 0)
}
DynamicColors.applyToActivitiesIfAvailable(this)
connectivityManager = getSystemService()!!
processLifecycleScope.launch(Dispatchers.IO) {
try {
@ -70,30 +74,29 @@ class App : Application(), Configuration.Provider {
}
}
// 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,
)
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,
)
}
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
workScheduleManager.init()
}
@ -123,8 +126,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,10 +3,12 @@ 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
@ -29,9 +31,10 @@ 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(content: @Composable () -> Unit) {
fun SettingsProvider(windowWidthSizeClass: WindowWidthSizeClass, content: @Composable () -> Unit) {
AppSettings.AppSettingsStateFlow.collectAsState().value.run {
CompositionLocalProvider(
LocalDarkTheme provides darkTheme,
@ -43,6 +46,7 @@ fun SettingsProvider(content: @Composable () -> Unit) {
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.screens.UpdateDialogImpl
import org.xtimms.shirizu.core.ui.dialogs.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 = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
val isCompactScreen = LocalWindowWidthState.current == WindowWidthSizeClass.Compact
val settings =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -141,7 +141,7 @@ class MainActivity : ComponentActivity() {
isReady.value = true
}
if (isReady.value) {
SettingsProvider {
SettingsProvider(windowSizeClass.widthSizeClass) {
ShirizuTheme(
darkTheme = LocalDarkTheme.current.isDarkTheme(),
isDynamicColorEnabled = LocalDynamicColorSwitch.current,

@ -0,0 +1,12 @@
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)
}

@ -66,6 +66,10 @@ 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
@ -232,6 +236,7 @@ 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) }
)
@ -308,6 +313,20 @@ 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,6 +63,13 @@ 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,181 @@
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
)
}
}

@ -0,0 +1,50 @@
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)
)
}
}
},
)
}

@ -0,0 +1,222 @@
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
}
}
}

@ -0,0 +1,327 @@
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()
}
}
}
}
}

@ -15,16 +15,17 @@ 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: ImageVector,
icon: Any? = null,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
@ -42,14 +43,29 @@ fun ExploreButton(
.height(40.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = text,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
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
)
}
}
Text(
text = text,
color = MaterialTheme.colorScheme.onSurface,

@ -0,0 +1,94 @@
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
)
}

@ -3,10 +3,6 @@ package org.xtimms.shirizu.core.components
import android.graphics.Path
import android.view.animation.PathInterpolator
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -14,39 +10,28 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
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.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.QueryStats
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RssFeed
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.SentimentSatisfiedAlt
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -54,35 +39,25 @@ import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
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.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Blue
import androidx.compose.ui.graphics.Color.Companion.DarkGray
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.DURATION_ENTER
@ -95,11 +70,10 @@ import org.xtimms.shirizu.sections.history.HISTORY_DESTINATION
import org.xtimms.shirizu.sections.search.SEARCH_DESTINATION
import org.xtimms.shirizu.sections.settings.SETTINGS_DESTINATION
import org.xtimms.shirizu.sections.shelf.SHELF_DESTINATION
import org.xtimms.shirizu.ui.theme.ShirizuTheme
import org.xtimms.shirizu.sections.stats.STATS_DESTINATION
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBar(
navController: NavController,
@ -108,6 +82,7 @@ fun TopAppBar(
searchBarColorProvider: () -> Color,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
var expanded by remember { mutableStateOf(false) }
val isVisible by remember {
derivedStateOf {
@ -203,15 +178,43 @@ fun TopAppBar(
)
}
IconButton(
onClick = { navController.navigate(SETTINGS_DESTINATION) },
onClick = { expanded = true },
modifier = Modifier.padding(0.dp),
) {
Icon(
Icons.Outlined.Settings,
contentDescription = stringResource(id = R.string.settings),
Icons.Outlined.MoreVert,
contentDescription = stringResource(id = R.string.open_menu),
tint = MaterialTheme.colorScheme.outline
)
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.statistics)) },
onClick = {
navController.navigate(STATS_DESTINATION)
expanded = false
},
leadingIcon = {
Icon(
imageVector = Icons.Default.QueryStats,
contentDescription = stringResource(id = R.string.statistics)
)
}
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.settings)) },
onClick = {
navController.navigate(SETTINGS_DESTINATION)
expanded = false
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(id = R.string.settings)
)
}
)
}
}
}
}

@ -0,0 +1,67 @@
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

@ -5,6 +5,7 @@ import androidx.room.Database
import androidx.room.InvalidationTracker
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -16,6 +17,7 @@ import org.xtimms.shirizu.core.database.dao.FavouritesDao
import org.xtimms.shirizu.core.database.dao.HistoryDao
import org.xtimms.shirizu.core.database.dao.MangaDao
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao
import org.xtimms.shirizu.core.database.dao.StatsDao
import org.xtimms.shirizu.core.database.dao.SuggestionDao
import org.xtimms.shirizu.core.database.dao.TagsDao
import org.xtimms.shirizu.core.database.dao.TrackLogsDao
@ -27,13 +29,15 @@ import org.xtimms.shirizu.core.database.entity.HistoryEntity
import org.xtimms.shirizu.core.database.entity.MangaEntity
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
import org.xtimms.shirizu.core.database.entity.MangaTagsEntity
import org.xtimms.shirizu.core.database.entity.StatsEntity
import org.xtimms.shirizu.core.database.entity.SuggestionEntity
import org.xtimms.shirizu.core.database.entity.TagEntity
import org.xtimms.shirizu.core.database.entity.TrackEntity
import org.xtimms.shirizu.core.database.entity.TrackLogEntity
import org.xtimms.shirizu.core.database.migrations.Migration1To2
import org.xtimms.shirizu.utils.lang.processLifecycleScope
const val DATABASE_VERSION = 1
const val DATABASE_VERSION = 2
@Database(
entities = [
@ -47,7 +51,8 @@ const val DATABASE_VERSION = 1
BookmarkEntity::class,
SuggestionEntity::class,
TrackEntity::class,
TrackLogEntity::class
TrackLogEntity::class,
StatsEntity::class,
],
version = DATABASE_VERSION
)
@ -73,10 +78,17 @@ abstract class ShirizuDatabase : RoomDatabase() {
abstract fun getTrackLogsDao(): TrackLogsDao
abstract fun getStatsDao(): StatsDao
}
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration1To2()
)
fun ShirizuDatabase(context: Context): ShirizuDatabase = Room
.databaseBuilder(context, ShirizuDatabase::class.java, "shirizu-db")
.addMigrations(*getDatabaseMigrations(context))
.addCallback(DatabasePrePopulateCallback(context.resources))
.build()

@ -0,0 +1,69 @@
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>
}

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

Loading…
Cancel
Save