feat: Onboarding screen (partial)

This commit is contained in:
Ahmad Ansori Palembani 2024-05-26 09:23:35 +07:00
parent 59b11b16e2
commit d086df7287
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
17 changed files with 987 additions and 10 deletions

View file

@ -147,15 +147,9 @@ android {
dependencies { dependencies {
// Compose // Compose
implementation(androidx.activity.compose) implementation(androidx.activity.compose)
implementation(compose.foundation) implementation(compose.bundles.compose)
implementation(compose.animation)
implementation(compose.ui)
debugImplementation(compose.ui.tooling) debugImplementation(compose.ui.tooling)
implementation(compose.ui.tooling.preview)
implementation(compose.material)
implementation(compose.material3)
implementation(libs.compose.theme.adapter3) implementation(libs.compose.theme.adapter3)
implementation(compose.icons)
implementation(libs.accompanist.webview) implementation(libs.accompanist.webview)
implementation(androidx.glance.appwidget) implementation(androidx.glance.appwidget)

View file

@ -2,6 +2,7 @@ package dev.yokai.domain.base
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.core.preference.getEnum import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
@ -17,4 +18,6 @@ class BasePreferences(private val preferenceStore: PreferenceStore) {
} }
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun hasShownOnboarding() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
} }

View file

@ -0,0 +1,65 @@
package dev.yokai.presentation.component
import android.graphics.Bitmap
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import eu.kanade.tachiyomi.R
@Composable
fun AppIcon(size: Dp, modifier: Modifier = Modifier) {
ResourcesCompat.getDrawable(
LocalContext.current.resources,
R.mipmap.ic_launcher,
LocalContext.current.theme
)
?.let { drawable ->
val bitmap =
Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = android.graphics.Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = modifier.requiredSize(size)
)
}
}
@Composable
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
val res = LocalContext.current.resources
val theme = LocalContext.current.theme
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android O supports adaptive icons, try loading this first (even though this is least
// likely to be the format).
val adaptiveIcon = ResourcesCompat.getDrawable(res, id, theme) as? AdaptiveIconDrawable
if (adaptiveIcon != null) {
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
} else {
// We couldn't load the drawable as an Adaptive Icon, just use painterResource
painterResource(id)
}
} else {
// We're not on Android O or later, just use painterResource
painterResource(id)
}
}

View file

@ -0,0 +1,332 @@
package dev.yokai.presentation.component
import android.content.Context
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
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.platform.LocalContext
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.LayoutDirection
import androidx.compose.ui.unit.dp
import com.google.accompanist.themeadapter.material3.createMdc3Theme
import dev.yokai.presentation.theme.HalfAlpha
import dev.yokai.presentation.theme.SecondaryItemAlpha
import dev.yokai.presentation.theme.Size
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.Themes
import eu.kanade.tachiyomi.util.system.isInNightMode
private data class ContextTheme(
val colorScheme: ColorScheme,
val isThemeMatchesApp: Boolean,
val theme: Themes,
val isDarkTheme: Boolean,
)
private fun Context.colorSchemeFromAdapter(theme: Themes, isDarkTheme: Boolean): ContextTheme {
val configuration = Configuration(this.resources.configuration)
configuration.uiMode =
if (isDarkTheme) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
val themeContext = this.createConfigurationContext(configuration)
themeContext.setTheme(theme.styleRes)
@Suppress("DEPRECATION") val colorScheme =
createMdc3Theme(
context = themeContext,
layoutDirection = LayoutDirection.Ltr,
setTextColors = true,
readTypography = false,
)
.colorScheme!!
val themeMatchesApp =
if (this.isInNightMode()) {
isDarkTheme
} else {
!isDarkTheme
}
return ContextTheme(colorScheme, themeMatchesApp, theme, isDarkTheme)
}
@Composable
fun ThemeItem(theme: Themes, isDarkTheme: Boolean, selected: Boolean, onClick: () -> Unit) {
val context = LocalContext.current
val contextTheme = context.colorSchemeFromAdapter(theme, isDarkTheme)
ThemeItemNaive(contextTheme = contextTheme, selected = selected, onClick = onClick)
}
@Composable
private fun ThemeItemNaive(contextTheme: ContextTheme, selected: Boolean, onClick: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.width(110.dp)) {
ThemePreviewItem(
contextTheme.colorScheme,
selected,
selectedColor = MaterialTheme.colorScheme.primary,
contextTheme.isThemeMatchesApp,
onClick
)
Text(
text = stringResource(id = if (contextTheme.isDarkTheme) contextTheme.theme.darkNameRes else contextTheme.theme.nameRes),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall
)
}
}
@Composable
fun ThemePreviewItem(
colorScheme: ColorScheme,
selected: Boolean,
selectedColor: Color,
themeMatchesApp: Boolean,
onClick: () -> Unit,
) {
val actualSelectedColor =
when {
themeMatchesApp && selected -> colorScheme.primary
selected -> selectedColor.copy(alpha = HalfAlpha)
else -> Color.Transparent
}
val padding = 6
val outer = 26
val inner = outer - padding
OutlinedCard(
onClick = onClick,
modifier = Modifier
.height(180.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(outer.dp),
border = BorderStroke(width = Size.tiny, color = actualSelectedColor),
) {
OutlinedCard(
modifier = Modifier
.height(180.dp)
.fillMaxWidth()
.padding(6.dp),
shape = RoundedCornerShape(inner.dp),
colors = CardDefaults.outlinedCardColors(containerColor = colorScheme.background),
border = BorderStroke(width = 1.dp, color = colorScheme.surfaceVariant),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.padding(Size.small),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.height(15.dp)
.weight(0.7f)
.padding(start = Size.tiny, end = Size.small)
.background(
color = colorScheme.onSurface,
shape = RoundedCornerShape(6.dp),
),
)
Box(
modifier = Modifier.weight(0.3f),
contentAlignment = Alignment.CenterEnd,
) {
if (selected) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = stringResource(R.string.selected),
tint = selectedColor,
)
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.weight(0.6f)
.padding(horizontal = Size.small),
) {
Box(
modifier = Modifier
.height(30.dp)
.fillMaxWidth()
.background(
color = colorScheme.onSurface.copy(alpha = SecondaryItemAlpha),
shape = RoundedCornerShape(8.dp),
),
)
Row(
modifier = Modifier
.height(30.dp)
.fillMaxWidth()
.padding(top = Size.small, end = Size.small),
) {
Box(
modifier = Modifier
.height(15.dp)
.weight(0.8f)
.padding(end = Size.tiny)
.background(
color = colorScheme.onSurface,
shape = RoundedCornerShape(6.dp),
),
)
Box(
modifier = Modifier
.height(15.dp)
.weight(0.3f)
.background(
color = colorScheme.secondary,
shape = RoundedCornerShape(6.dp),
),
)
}
Row(
modifier = Modifier
.height(15.dp)
.fillMaxWidth()
.padding(end = Size.medium),
) {
Box(
modifier = Modifier
.fillMaxHeight()
.weight(0.5f)
.padding(end = Size.tiny)
.background(
color = colorScheme.onSurface,
shape = RoundedCornerShape(6.dp),
),
)
Box(
modifier = Modifier
.fillMaxHeight()
.weight(0.6f)
.background(
color = colorScheme.onSurface,
shape = RoundedCornerShape(6.dp),
),
)
}
}
Surface(
color = colorScheme.surfaceVariant,
tonalElevation = Size.small,
) {
Row(
modifier = Modifier
.height(30.dp)
.fillMaxWidth()
.padding(vertical = Size.extraTiny, horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.fillMaxHeight()
.weight(0.2f),
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
.background(
color = colorScheme.onSurface.copy(alpha = SecondaryItemAlpha),
shape = CircleShape,
),
)
}
Box(
modifier = Modifier
.fillMaxHeight()
.weight(0.2f),
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
.background(
color = colorScheme.secondary,
shape = CircleShape,
),
)
}
Box(
modifier = Modifier
.fillMaxHeight()
.weight(0.2f),
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
.background(
color = colorScheme.onSurface.copy(alpha = SecondaryItemAlpha),
shape = CircleShape,
),
)
}
}
}
}
}
}
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun ThemeItemPreviewDark() {
val contextTheme = ContextTheme(
colorScheme = darkColorScheme(),
isThemeMatchesApp = true,
theme = Themes.DEFAULT,
isDarkTheme = true,
)
Surface {
ThemeItemNaive(contextTheme = contextTheme, selected = true) {}
}
}
@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true)
@Composable
fun ThemeItemPreviewLight() {
val contextTheme = ContextTheme(
colorScheme = lightColorScheme(),
isThemeMatchesApp = true,
theme = Themes.DEFAULT,
isDarkTheme = false,
)
Surface {
ThemeItemNaive(contextTheme = contextTheme, selected = false) {}
}
}

View file

@ -0,0 +1,7 @@
package dev.yokai.presentation.core.util
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import dev.yokai.presentation.theme.SecondaryItemAlpha
fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha)

View file

@ -0,0 +1,184 @@
package dev.yokai.presentation.onboarding
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.RocketLaunch
import androidx.compose.material.icons.outlined.RocketLaunch
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.zIndex
import dev.yokai.presentation.core.util.secondaryItemAlpha
import dev.yokai.presentation.theme.Size
@Composable
fun InfoScreen(
icon: ImageVector,
headingText: String,
subtitleText: String,
tint: Color = MaterialTheme.colorScheme.primary,
acceptText: String,
onAcceptClick: () -> Unit,
canAccept: Boolean,
rejectText: String? = null,
onRejectClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
InfoScreen(
icon = {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier
.padding(bottom = Size.small)
.size(Size.huge),
tint = tint,
)
},
headingText = headingText,
subtitleText = subtitleText,
acceptText = acceptText,
onAcceptClick = onAcceptClick,
canAccept = canAccept,
rejectText = rejectText,
onRejectClick = onRejectClick,
content = content,
)
}
@Composable
fun InfoScreen(
icon: @Composable () -> Unit,
headingText: String,
subtitleText: String,
tint: Color = MaterialTheme.colorScheme.primary,
acceptText: String,
onAcceptClick: () -> Unit,
canAccept: Boolean,
rejectText: String? = null,
onRejectClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Scaffold(
bottomBar = {
val strokeWidth = Dp.Hairline
val borderColor = MaterialTheme.colorScheme.outline
Column(
modifier =
Modifier
.background(MaterialTheme.colorScheme.background)
.drawBehind {
drawLine(
borderColor,
Offset(0f, 0f),
Offset(size.width, 0f),
strokeWidth.value,
)
}
.windowInsetsPadding(NavigationBarDefaults.windowInsets)
.padding(
horizontal = Size.medium,
vertical = Size.small,
),
) {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = canAccept,
colors = ButtonDefaults.buttonColors(containerColor = tint),
onClick = onAcceptClick,
) {
Text(text = acceptText)
}
if (rejectText != null && onRejectClick != null) {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
border =
BorderStroke(
width = Size.extraExtraTiny,
color = tint,
),
onClick = onRejectClick,
) {
Text(text = rejectText)
}
}
}
},
) { paddingValues ->
// Status bar scrim
Box(
modifier = Modifier
.zIndex(2f)
.secondaryItemAlpha()
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(paddingValues.calculateTopPadding()),
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(paddingValues)
.padding(top = Size.huge)
.padding(horizontal = Size.medium),
) {
icon()
Text(
text = headingText,
style = MaterialTheme.typography.headlineLarge,
)
Text(
text = subtitleText,
modifier = Modifier
.secondaryItemAlpha()
.padding(vertical = Size.small),
style = MaterialTheme.typography.titleSmall,
)
content()
}
}
}
@Preview
@Composable
private fun InfoScreenPreview() {
InfoScreen(
icon = Icons.Outlined.RocketLaunch,
headingText = "Welcome!",
subtitleText = "Subtitle",
acceptText = "Accept",
onAcceptClick = {},
canAccept = true,
rejectText = "Reject",
onRejectClick = {},
) {
Text(text = "Hello World")
}
}

View file

@ -0,0 +1,37 @@
package dev.yokai.presentation.onboarding
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.core.preference.collectAsState
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import uy.kohesive.injekt.injectLazy
class OnboardingController :
BaseComposeController() {
val basePreferences by injectLazy<BasePreferences>()
@Composable
override fun ScreenContent() {
val hasShownOnboarding by basePreferences.hasShownOnboarding().collectAsState()
val finishOnboarding: () -> Unit = {
basePreferences.hasShownOnboarding().set(true)
router.popCurrentController()
}
BackHandler(
enabled = !hasShownOnboarding,
onBack = {
// Prevent exiting if onboarding hasn't been completed
},
)
OnboardingScreen(
onComplete = finishOnboarding
)
}
}

View file

@ -0,0 +1,90 @@
package dev.yokai.presentation.onboarding
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.RocketLaunch
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import dev.yokai.presentation.onboarding.steps.ThemeStep
import dev.yokai.presentation.theme.Size
import eu.kanade.tachiyomi.R
import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance
@SuppressLint("UnusedContentLambdaTargetStateParameter")
@Composable
fun OnboardingScreen(
onComplete: () -> Unit = {}
) {
val slideDistance = rememberSlideDistance()
var currentStep by rememberSaveable { mutableIntStateOf(0) }
val steps = remember {
listOf(
ThemeStep(),
ThemeStep(),
ThemeStep(),
)
}
val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
InfoScreen(
icon = Icons.Outlined.RocketLaunch,
headingText = stringResource(R.string.onboarding_heading),
subtitleText = stringResource(R.string.onboarding_description),
tint = MaterialTheme.colorScheme.primary,
acceptText = stringResource(
if (isLastStep)
R.string.onboarding_finish
else {
R.string.next
}
),
canAccept = steps[currentStep].isComplete,
onAcceptClick = {
if (isLastStep) {
onComplete()
} else {
currentStep++
}
},
) {
Box(
modifier = Modifier
.padding(vertical = Size.small)
.clip(MaterialTheme.shapes.small)
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
AnimatedContent(
targetState = currentStep,
transitionSpec = {
materialSharedAxisX(
forward = targetState > initialState,
slideDistance = slideDistance,
)
},
label = "stepContent",
) {
steps[currentStep].Content()
}
}
}
}

View file

@ -0,0 +1,11 @@
package dev.yokai.presentation.onboarding.steps
import androidx.compose.runtime.Composable
internal interface OnboardingStep {
val isComplete: Boolean
@Composable
fun Content()
}

View file

@ -0,0 +1,212 @@
package dev.yokai.presentation.onboarding.steps
import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.horizontalScroll
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.rememberScrollState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import com.google.android.material.color.DynamicColors
import dev.yokai.presentation.component.ThemeItem
import dev.yokai.presentation.theme.Size
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.collectAsState
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.Themes
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
import uy.kohesive.injekt.injectLazy
class ThemeStep : OnboardingStep {
override val isComplete: Boolean = true
private val preferences: PreferencesHelper by injectLazy()
@Composable
override fun Content() {
val context = LocalContext.current
val nightModePreference = preferences.nightMode()
val nightMode by nightModePreference.collectAsState()
val followingSystemTheme by remember(nightMode) {
derivedStateOf { nightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM }
}
val darkAppTheme by preferences.darkTheme().collectAsState()
val lightAppTheme by preferences.lightTheme().collectAsState()
val supportsDynamic = DynamicColors.isDynamicColorAvailable()
Themes.entries
.filter {
(!it.isDarkTheme || it.followsSystem) &&
(it.styleRes != R.style.Theme_Tachiyomi_Monet || supportsDynamic)
}
.toSet()
val lightThemes by remember {
derivedStateOf {
Themes.entries
.filter {
(!it.isDarkTheme || it.followsSystem) &&
(it.styleRes != R.style.Theme_Tachiyomi_Monet || supportsDynamic)
}
.toSet()
}
}
val darkThemes by remember {
derivedStateOf {
Themes.entries
.filter {
(it.isDarkTheme || it.followsSystem) &&
(it.styleRes != R.style.Theme_Tachiyomi_Monet || supportsDynamic)
}
.toSet()
}
}
Column(modifier = Modifier.padding(Size.medium)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(id = R.string.follow_system_theme))
Switch(
checked = nightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
colors =
SwitchDefaults.colors(
checkedTrackColor = MaterialTheme.colorScheme.primary
),
onCheckedChange = {
when (it) {
true -> {
preferences
.nightMode()
.set(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
(context as? Activity)?.let { activity ->
ActivityCompat.recreate(activity)
}
}
false -> preferences.nightMode().set(context.appDelegateNightMode())
}
}
)
}
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(Size.medium)
) {
lightThemes.forEach { theme ->
val isSelected =
remember(darkAppTheme, lightAppTheme, nightMode) {
isSelected(theme, false, darkAppTheme, lightAppTheme, nightMode)
}
ThemeItem(
theme = theme,
isDarkTheme = false,
selected = isSelected,
onClick = {
themeClicked(
theme,
context,
isSelected = isSelected,
followingSystemTheme = followingSystemTheme,
isDarkTheme = false
)
}
)
}
}
Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(Size.medium)
) {
darkThemes.forEach { theme ->
val isSelected =
remember(darkAppTheme, lightAppTheme, nightMode) {
isSelected(theme, true, darkAppTheme, lightAppTheme, nightMode)
}
ThemeItem(
theme = theme,
isDarkTheme = true,
selected = isSelected,
onClick = {
themeClicked(
theme,
context,
isSelected = isSelected,
followingSystemTheme = followingSystemTheme,
isDarkTheme = true
)
}
)
}
}
}
}
private fun isSelected(
theme: Themes,
isDarkTheme: Boolean,
darkAppTheme: Themes,
lightAppTheme: Themes,
nightMode: Int
): Boolean {
return when (nightMode) {
AppCompatDelegate.MODE_NIGHT_YES -> darkAppTheme == theme && isDarkTheme
AppCompatDelegate.MODE_NIGHT_NO -> lightAppTheme == theme && !isDarkTheme
else ->
(darkAppTheme == theme && isDarkTheme) || (lightAppTheme == theme && !isDarkTheme)
}
}
private fun themeClicked(
theme: Themes,
context: Context,
isSelected: Boolean,
followingSystemTheme: Boolean,
isDarkTheme: Boolean
) {
val nightMode =
when (isDarkTheme) {
true -> {
preferences.darkTheme().set(theme)
AppCompatDelegate.MODE_NIGHT_YES
}
false -> {
preferences.lightTheme().set(theme)
AppCompatDelegate.MODE_NIGHT_NO
}
}
if (followingSystemTheme && isSelected) {
preferences.nightMode().set(nightMode)
} else if (!followingSystemTheme) {
preferences.nightMode().set(nightMode)
}
(context as? Activity)?.let { activity -> ActivityCompat.recreate(activity) }
}
}

View file

@ -0,0 +1,21 @@
package dev.yokai.presentation.theme
import androidx.compose.ui.unit.dp
const val SecondaryItemAlpha = .78f
const val HalfAlpha = .5f
object Size {
val none = 0.dp
val extraExtraTiny = 1.dp
val extraTiny = 2.dp
val tiny = 4.dp
val small = 8.dp
val smedium = 12.dp
val medium = 16.dp
val large = 24.dp
val extraLarge = 32.dp
val huge = 48.dp
val extraHuge = 56.dp
val navBarSize = 68.dp
}

View file

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.core.preference package eu.kanade.tachiyomi.core.preference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -73,3 +77,9 @@ fun Preference<Boolean>.toggle(): Boolean {
set(!get()) set(!get())
return get() return get()
} }
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
}

View file

@ -76,6 +76,7 @@ import com.google.common.primitives.Ints.max
import dev.yokai.domain.base.BasePreferences import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.ui.settings.ReaderPreferences import dev.yokai.domain.ui.settings.ReaderPreferences
import dev.yokai.presentation.extension.repo.ExtensionRepoController import dev.yokai.presentation.extension.repo.ExtensionRepoController
import dev.yokai.presentation.onboarding.OnboardingController
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -519,6 +520,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
// Set start screen // Set start screen
if (!handleIntentAction(intent)) { if (!handleIntentAction(intent)) {
goToStartingTab() goToStartingTab()
if (!basePreferences.hasShownOnboarding().get()) {
router.pushController(OnboardingController().withFadeInTransaction())
}
} }
} }

View file

@ -8,6 +8,10 @@
<string name="external_storage_permission_notice">TachiyomiJ2K requires access to all files in Android 11 to download chapters, create automatic backups, and read local series. \n\nOn the next screen, enable \"Allow access to manage all files.\"</string> <string name="external_storage_permission_notice">TachiyomiJ2K requires access to all files in Android 11 to download chapters, create automatic backups, and read local series. \n\nOn the next screen, enable \"Allow access to manage all files.\"</string>
<string name="external_storage_download_notice">TachiyomiJ2K requires access to all files to download chapters. Tap here, then enable \"Allow access to manage all files.\"</string> <string name="external_storage_download_notice">TachiyomiJ2K requires access to all files to download chapters. Tap here, then enable \"Allow access to manage all files.\"</string>
<string name="onboarding_heading">Welcome!</string>
<string name="onboarding_description">Let\'s pick some defaults. You can always change these things later in the settings.</string>
<string name="onboarding_finish">Get started</string>
<!--Models--> <!--Models-->
<!-- Manga Type --> <!-- Manga Type -->

View file

@ -22,6 +22,5 @@ android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M org.gradle.jvmargs=-Xmx2048M
org.gradle.caching=true org.gradle.caching=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false

View file

@ -8,8 +8,12 @@ animation = { module = "androidx.compose.animation:animation", version.ref = "co
foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
material = { module = "androidx.compose.material:material", version.ref = "compose" } material = { module = "androidx.compose.material:material", version.ref = "compose" }
material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" } material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" }
material-motion = { module = "io.github.fornewid:material-motion-compose-core", version = "1.0.7" }
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
[bundles]
compose = [ "animation", "foundation", "material", "material3", "material-motion", "ui", "ui-tooling-preview", "icons" ]

View file

@ -29,7 +29,7 @@ flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" }
flexible-adapter-ui = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui", version.ref = "flexible-adapter" } flexible-adapter-ui = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui", version.ref = "flexible-adapter" }
flexible-adapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexible-adapter" } flexible-adapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexible-adapter" }
google-services = { module = "com.google.gms:google-services", version = "4.4.1" } google-services = { module = "com.google.gms:google-services", version = "4.4.1" }
gradle = { module = "com.android.tools.build:gradle", version = "8.1.4" } gradle = { module = "com.android.tools.build:gradle", version = "8.2.0" }
guava = { module = "com.google.guava:guava", version = "31.1-android" } guava = { module = "com.google.guava:guava", version = "31.1-android" }
image-decoder = { module = "com.github.tachiyomiorg:image-decoder", version = "e08e9be535" } image-decoder = { module = "com.github.tachiyomiorg:image-decoder", version = "e08e9be535" }
injekt-core = { module = "com.github.inorichi.injekt:injekt-core", version = "65b0440" } injekt-core = { module = "com.github.inorichi.injekt:injekt-core", version = "65b0440" }