mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
feat: Onboarding screen (partial)
This commit is contained in:
parent
59b11b16e2
commit
d086df7287
17 changed files with 987 additions and 10 deletions
|
@ -147,15 +147,9 @@ android {
|
|||
dependencies {
|
||||
// Compose
|
||||
implementation(androidx.activity.compose)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.animation)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.bundles.compose)
|
||||
debugImplementation(compose.ui.tooling)
|
||||
implementation(compose.ui.tooling.preview)
|
||||
implementation(compose.material)
|
||||
implementation(compose.material3)
|
||||
implementation(libs.compose.theme.adapter3)
|
||||
implementation(compose.icons)
|
||||
implementation(libs.accompanist.webview)
|
||||
implementation(androidx.glance.appwidget)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package dev.yokai.domain.base
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
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.getEnum
|
||||
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 hasShownOnboarding() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
332
app/src/main/java/dev/yokai/presentation/component/ThemeItem.kt
Normal file
332
app/src/main/java/dev/yokai/presentation/component/ThemeItem.kt
Normal 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) {}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package dev.yokai.presentation.onboarding.steps
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
internal interface OnboardingStep {
|
||||
|
||||
val isComplete: Boolean
|
||||
|
||||
@Composable
|
||||
fun Content()
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
21
app/src/main/java/dev/yokai/presentation/theme/Constants.kt
Normal file
21
app/src/main/java/dev/yokai/presentation/theme/Constants.kt
Normal 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
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -73,3 +77,9 @@ fun Preference<Boolean>.toggle(): Boolean {
|
|||
set(!get())
|
||||
return get()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ import com.google.common.primitives.Ints.max
|
|||
import dev.yokai.domain.base.BasePreferences
|
||||
import dev.yokai.domain.ui.settings.ReaderPreferences
|
||||
import dev.yokai.presentation.extension.repo.ExtensionRepoController
|
||||
import dev.yokai.presentation.onboarding.OnboardingController
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.Migrations
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@ -519,6 +520,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
|
|||
// Set start screen
|
||||
if (!handleIntentAction(intent)) {
|
||||
goToStartingTab()
|
||||
if (!basePreferences.hasShownOnboarding().get()) {
|
||||
router.pushController(OnboardingController().withFadeInTransaction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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_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-->
|
||||
|
||||
<!-- Manga Type -->
|
||||
|
|
|
@ -22,6 +22,5 @@ android.enableJetifier=true
|
|||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.caching=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
|
@ -8,8 +8,12 @@ animation = { module = "androidx.compose.animation:animation", version.ref = "co
|
|||
foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
|
||||
material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
||||
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" }
|
||||
ui = { module = "androidx.compose.ui:ui", 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" }
|
||||
icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
|
||||
|
||||
[bundles]
|
||||
compose = [ "animation", "foundation", "material", "material3", "material-motion", "ui", "ui-tooling-preview", "icons" ]
|
||||
|
|
|
@ -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 = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexible-adapter" }
|
||||
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" }
|
||||
image-decoder = { module = "com.github.tachiyomiorg:image-decoder", version = "e08e9be535" }
|
||||
injekt-core = { module = "com.github.inorichi.injekt:injekt-core", version = "65b0440" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue