Compare commits

...

4 Commits

@ -16,6 +16,32 @@
</AndroidTestResultsTableState> </AndroidTestResultsTableState>
</value> </value>
</entry> </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> </map>
</option> </option>
</component> </component>

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

@ -15,8 +15,10 @@ plugins {
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
} }
val acraAuthLogin: String = gradleLocalProperties(rootDir).getProperty("authLogin") ?: "\"acra_login\"" val acraAuthLogin: String =
val acraAuthPassword: String = gradleLocalProperties(rootDir).getProperty("authPassword") ?: "\"acra_password\"" gradleLocalProperties(rootDir).getProperty("authLogin") ?: "\"acra_login\""
val acraAuthPassword: String =
gradleLocalProperties(rootDir).getProperty("authPassword") ?: "\"acra_password\""
android { android {
namespace = "org.xtimms.shirizu" namespace = "org.xtimms.shirizu"
@ -41,27 +43,45 @@ android {
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
javaCompileOptions { ksp {
annotationProcessorOptions { arg("room.schemaLocation", "$projectDir/schemas")
arguments += mapOf(
"room.generateKotlin" to "true",
"room.schemaLocation" to "$projectDir/schemas"
)
}
} }
} }
buildTypes { buildTypes {
debug { named("debug") {
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
} }
release { named("release") {
isShrinkResources = true
isMinifyEnabled = true isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "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 { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
@ -111,6 +131,7 @@ dependencies {
implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.work:work-runtime-ktx:2.9.0") 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") ksp("androidx.room:room-compiler:2.6.1")
implementation("ch.acra:acra-http:5.9.7") implementation("ch.acra:acra-http:5.9.7")
implementation("com.github.solkin:disk-lru-cache:1.4") 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-systemuicontroller:0.32.0")
implementation("com.google.accompanist:accompanist-pager:0.32.0") implementation("com.google.accompanist:accompanist-pager:0.32.0")
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0") implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
implementation("com.google.dagger:hilt-android:2.51") implementation("com.google.dagger:hilt-android:2.51")
kapt("com.google.dagger:hilt-compiler:2.51") kapt("com.google.dagger:hilt-compiler:2.51")
implementation("androidx.hilt:hilt-work:1.2.0") implementation("androidx.hilt:hilt-work:1.2.0")
@ -149,6 +171,15 @@ dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") 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. // 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 // If it's not installed, you can return a random value as a workaround
fun Project.getCommitCount(): String { 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:name=".App"
android:allowBackup="false" android:allowBackup="false"
android:backupAgent=".sections.settings.backup.AppBackupAgent" android:backupAgent=".sections.settings.backup.AppBackupAgent"
android:enableOnBackInvokedCallback="true"
android:hardwareAccelerated="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Shirizu" android:theme="@style/Theme.Shirizu"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
@ -50,7 +50,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".crash.CrashActivity" android:name=".crash.CrashActivity"
android:exported="false" android:exported="false"
@ -70,7 +69,6 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
@ -91,7 +89,6 @@
<meta-data <meta-data
android:name="com.samsung.android.icon_container.has_icon_container" android:name="com.samsung.android.icon_container.has_icon_container"
android:value="@bool/com_samsung_android_icon_container_has_icon_container" /> android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
</application> </application>
</manifest> </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.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Build import android.os.Build
import android.os.StrictMode import android.os.StrictMode
import androidx.core.content.getSystemService
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
@ -21,9 +23,10 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.xtimms.shirizu.core.database.ShirizuDatabase 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.prefs.KotatsuAppSettings
import org.xtimms.shirizu.core.updates.Updater 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.utils.lang.processLifecycleScope
import org.xtimms.shirizu.work.WorkScheduleManager import org.xtimms.shirizu.work.WorkScheduleManager
import javax.inject.Inject import javax.inject.Inject
@ -61,6 +64,7 @@ class App : Application(), Configuration.Provider {
) else getPackageInfo(packageName, 0) ) else getPackageInfo(packageName, 0)
} }
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
connectivityManager = getSystemService()!!
processLifecycleScope.launch(Dispatchers.IO) { processLifecycleScope.launch(Dispatchers.IO) {
try { try {
@ -70,8 +74,6 @@ class App : Application(), Configuration.Provider {
} }
} }
// GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
if (AppSettings.isACRAEnabled()) {
initAcra { initAcra {
buildConfigClass = BuildConfig::class.java buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
@ -93,7 +95,8 @@ class App : Application(), Configuration.Provider {
ReportField.CUSTOM_DATA, ReportField.CUSTOM_DATA,
) )
} }
}
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
workScheduleManager.init() workScheduleManager.init()
} }
@ -123,8 +126,8 @@ class App : Application(), Configuration.Provider {
companion object { companion object {
lateinit var packageInfo: PackageInfo lateinit var packageInfo: PackageInfo
lateinit var connectivityManager: ConnectivityManager
@Suppress("DEPRECATION")
fun getVersionReport(): String { fun getVersionReport(): String {
val versionName = packageInfo.versionName val versionName = packageInfo.versionName
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

@ -3,10 +3,12 @@ package org.xtimms.shirizu
import android.os.Build import android.os.Build
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -29,9 +31,10 @@ val LocalSeedColor = compositionLocalOf { SEED }
val LocalDynamicColorSwitch = compositionLocalOf { false } val LocalDynamicColorSwitch = compositionLocalOf { false }
val LocalPaletteStyleIndex = compositionLocalOf { 0 } val LocalPaletteStyleIndex = compositionLocalOf { 0 }
val LocalWindowInsets = compositionLocalOf { PaddingValues(0.dp) } val LocalWindowInsets = compositionLocalOf { PaddingValues(0.dp) }
val LocalWindowWidthState = staticCompositionLocalOf { WindowWidthSizeClass.Compact }
@Composable @Composable
fun SettingsProvider(content: @Composable () -> Unit) { fun SettingsProvider(windowWidthSizeClass: WindowWidthSizeClass, content: @Composable () -> Unit) {
AppSettings.AppSettingsStateFlow.collectAsState().value.run { AppSettings.AppSettingsStateFlow.collectAsState().value.run {
CompositionLocalProvider( CompositionLocalProvider(
LocalDarkTheme provides darkTheme, LocalDarkTheme provides darkTheme,
@ -43,6 +46,7 @@ fun SettingsProvider(content: @Composable () -> Unit) {
else Color(seedColor).toTonalPalettes( else Color(seedColor).toTonalPalettes(
paletteStyles.getOrElse(paletteStyleIndex) { PaletteStyle.TonalSpot } paletteStyles.getOrElse(paletteStyleIndex) { PaletteStyle.TonalSpot }
), ),
LocalWindowWidthState provides windowWidthSizeClass,
LocalDynamicColorSwitch provides isDynamicColorEnabled, LocalDynamicColorSwitch provides isDynamicColorEnabled,
content = content 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.components.TopAppBar
import org.xtimms.shirizu.core.logs.FileLogger import org.xtimms.shirizu.core.logs.FileLogger
import org.xtimms.shirizu.core.prefs.AppSettings 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.core.updates.Updater
import org.xtimms.shirizu.ui.theme.ShirizuTheme import org.xtimms.shirizu.ui.theme.ShirizuTheme
import org.xtimms.shirizu.utils.system.setLanguage import org.xtimms.shirizu.utils.system.setLanguage
@ -112,7 +112,7 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController() val navController = rememberNavController()
val windowSizeClass = calculateWindowSizeClass(this) val windowSizeClass = calculateWindowSizeClass(this)
val isCompactScreen = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact val isCompactScreen = LocalWindowWidthState.current == WindowWidthSizeClass.Compact
val settings = val settings =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@ -141,7 +141,7 @@ class MainActivity : ComponentActivity() {
isReady.value = true isReady.value = true
} }
if (isReady.value) { if (isReady.value) {
SettingsProvider { SettingsProvider(windowSizeClass.widthSizeClass) {
ShirizuTheme( ShirizuTheme(
darkTheme = LocalDarkTheme.current.isDarkTheme(), darkTheme = LocalDarkTheme.current.isDarkTheme(),
isDynamicColorEnabled = LocalDynamicColorSwitch.current, 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.backup.RestoreItemsView
import org.xtimms.shirizu.sections.settings.network.NETWORK_DESTINATION import org.xtimms.shirizu.sections.settings.network.NETWORK_DESTINATION
import org.xtimms.shirizu.sections.settings.network.NetworkView 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.SHELF_SETTINGS_DESTINATION
import org.xtimms.shirizu.sections.settings.shelf.ShelfSettingsView import org.xtimms.shirizu.sections.settings.shelf.ShelfSettingsView
import org.xtimms.shirizu.sections.settings.shelf.categories.CATEGORIES_DESTINATION import org.xtimms.shirizu.sections.settings.shelf.categories.CATEGORIES_DESTINATION
@ -232,6 +236,7 @@ fun Navigation(
}, },
navigateToMangaSources = { navController.navigate(SOURCES_DESTINATION) }, navigateToMangaSources = { navController.navigate(SOURCES_DESTINATION) },
navigateToNetwork = { navController.navigate(NETWORK_DESTINATION) }, navigateToNetwork = { navController.navigate(NETWORK_DESTINATION) },
navigateToServicesSettings = { navController.navigate(SERVICES_DESTINATION) },
navigateToShelfSettings = { navController.navigate(SHELF_SETTINGS_DESTINATION) }, navigateToShelfSettings = { navController.navigate(SHELF_SETTINGS_DESTINATION) },
navigateToStorage = { navController.navigate(STORAGE_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) { composable(NETWORK_DESTINATION) {
NetworkView( NetworkView(
navigateBack = navigateBack, navigateBack = navigateBack,

@ -63,6 +63,13 @@ abstract class KotatsuBaseViewModel : ViewModel() {
loadingCounter.decrement() 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 -> protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
errorEvent.call(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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.xtimms.shirizu.ui.theme.applyOpacity
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ExploreButton( fun ExploreButton(
text: String, text: String,
icon: ImageVector, icon: Any? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -42,6 +43,8 @@ fun ExploreButton(
.height(40.dp), .height(40.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
when (icon) {
is ImageVector -> {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = text, contentDescription = text,
@ -50,6 +53,19 @@ fun ExploreButton(
.size(24.dp), .size(24.dp),
tint = MaterialTheme.colorScheme.primary 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 = text, text = text,
color = MaterialTheme.colorScheme.onSurface, 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.graphics.Path
import android.view.animation.PathInterpolator import android.view.animation.PathInterpolator
import androidx.compose.animation.AnimatedVisibility 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.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@ -14,39 +10,28 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.RowScope 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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.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.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.RssFeed
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.SentimentSatisfiedAlt import androidx.compose.material.icons.outlined.SentimentSatisfiedAlt
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -54,35 +39,25 @@ import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import org.xtimms.shirizu.R import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.DURATION_ENTER 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.search.SEARCH_DESTINATION
import org.xtimms.shirizu.sections.settings.SETTINGS_DESTINATION import org.xtimms.shirizu.sections.settings.SETTINGS_DESTINATION
import org.xtimms.shirizu.sections.shelf.SHELF_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.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TopAppBar( fun TopAppBar(
navController: NavController, navController: NavController,
@ -108,6 +82,7 @@ fun TopAppBar(
searchBarColorProvider: () -> Color, searchBarColorProvider: () -> Color,
) { ) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
var expanded by remember { mutableStateOf(false) }
val isVisible by remember { val isVisible by remember {
derivedStateOf { derivedStateOf {
@ -203,15 +178,43 @@ fun TopAppBar(
) )
} }
IconButton( IconButton(
onClick = { navController.navigate(SETTINGS_DESTINATION) }, onClick = { expanded = true },
modifier = Modifier.padding(0.dp), modifier = Modifier.padding(0.dp),
) { ) {
Icon( Icon(
Icons.Outlined.Settings, Icons.Outlined.MoreVert,
contentDescription = stringResource(id = R.string.settings), contentDescription = stringResource(id = R.string.open_menu),
tint = MaterialTheme.colorScheme.outline 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.InvalidationTracker
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi 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.HistoryDao
import org.xtimms.shirizu.core.database.dao.MangaDao import org.xtimms.shirizu.core.database.dao.MangaDao
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao 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.SuggestionDao
import org.xtimms.shirizu.core.database.dao.TagsDao import org.xtimms.shirizu.core.database.dao.TagsDao
import org.xtimms.shirizu.core.database.dao.TrackLogsDao 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.MangaEntity
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
import org.xtimms.shirizu.core.database.entity.MangaTagsEntity 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.SuggestionEntity
import org.xtimms.shirizu.core.database.entity.TagEntity import org.xtimms.shirizu.core.database.entity.TagEntity
import org.xtimms.shirizu.core.database.entity.TrackEntity import org.xtimms.shirizu.core.database.entity.TrackEntity
import org.xtimms.shirizu.core.database.entity.TrackLogEntity import org.xtimms.shirizu.core.database.entity.TrackLogEntity
import org.xtimms.shirizu.core.database.migrations.Migration1To2
import org.xtimms.shirizu.utils.lang.processLifecycleScope import org.xtimms.shirizu.utils.lang.processLifecycleScope
const val DATABASE_VERSION = 1 const val DATABASE_VERSION = 2
@Database( @Database(
entities = [ entities = [
@ -47,7 +51,8 @@ const val DATABASE_VERSION = 1
BookmarkEntity::class, BookmarkEntity::class,
SuggestionEntity::class, SuggestionEntity::class,
TrackEntity::class, TrackEntity::class,
TrackLogEntity::class TrackLogEntity::class,
StatsEntity::class,
], ],
version = DATABASE_VERSION version = DATABASE_VERSION
) )
@ -73,10 +78,17 @@ abstract class ShirizuDatabase : RoomDatabase() {
abstract fun getTrackLogsDao(): TrackLogsDao abstract fun getTrackLogsDao(): TrackLogsDao
abstract fun getStatsDao(): StatsDao
} }
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration1To2()
)
fun ShirizuDatabase(context: Context): ShirizuDatabase = Room fun ShirizuDatabase(context: Context): ShirizuDatabase = Room
.databaseBuilder(context, ShirizuDatabase::class.java, "shirizu-db") .databaseBuilder(context, ShirizuDatabase::class.java, "shirizu-db")
.addMigrations(*getDatabaseMigrations(context))
.addCallback(DatabasePrePopulateCallback(context.resources)) .addCallback(DatabasePrePopulateCallback(context.resources))
.build() .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