Explorar el Código

Add basic onboarding screen (#10199)

arkon hace 1 año
padre
commit
8b57169e92

+ 2 - 0
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt

@@ -24,6 +24,8 @@ class BasePreferences(
 
 
     fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
     fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
 
 
+    fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
+
     enum class ExtensionInstaller(val titleRes: StringResource) {
     enum class ExtensionInstaller(val titleRes: StringResource) {
         LEGACY(MR.strings.ext_installer_legacy),
         LEGACY(MR.strings.ext_installer_legacy),
         PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
         PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),

+ 68 - 0
app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt

@@ -0,0 +1,68 @@
+package eu.kanade.presentation.more.onboarding
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.RocketLaunch
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import eu.kanade.domain.ui.UiPreferences
+import soup.compose.material.motion.animation.materialSharedAxisX
+import soup.compose.material.motion.animation.rememberSlideDistance
+import tachiyomi.domain.storage.service.StoragePreferences
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.i18n.stringResource
+import tachiyomi.presentation.core.screens.InfoScreen
+
+@Composable
+fun OnboardingScreen(
+    storagePreferences: StoragePreferences,
+    uiPreferences: UiPreferences,
+    onComplete: () -> Unit,
+) {
+    var currentStep by remember { mutableIntStateOf(0) }
+    val steps: List<@Composable () -> Unit> = listOf(
+        { ThemeStep(uiPreferences = uiPreferences) },
+        { StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
+        // TODO: prompt for notification permissions when bumping target to Android 13
+    )
+    val isLastStep = currentStep == steps.size - 1
+    val slideDistance = rememberSlideDistance()
+
+    InfoScreen(
+        icon = Icons.Outlined.RocketLaunch,
+        headingText = stringResource(MR.strings.onboarding_heading),
+        subtitleText = stringResource(MR.strings.onboarding_description),
+        acceptText = stringResource(
+            if (isLastStep) {
+                MR.strings.onboarding_action_finish
+            } else {
+                MR.strings.onboarding_action_next
+            },
+        ),
+        onAcceptClick = {
+            if (!isLastStep) {
+                currentStep++
+            } else {
+                onComplete()
+            }
+        },
+        rejectText = stringResource(MR.strings.onboarding_action_skip),
+        onRejectClick = onComplete,
+    ) {
+        AnimatedContent(
+            targetState = currentStep,
+            transitionSpec = {
+                materialSharedAxisX(
+                    forward = true,
+                    slideDistance = slideDistance,
+                )
+            },
+            label = "stepContent",
+        ) {
+            steps[it]()
+        }
+    }
+}

+ 41 - 0
app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt

@@ -0,0 +1,41 @@
+package eu.kanade.presentation.more.onboarding
+
+import android.content.ActivityNotFoundException
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
+import eu.kanade.tachiyomi.util.system.toast
+import tachiyomi.core.preference.Preference
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.components.material.Button
+import tachiyomi.presentation.core.i18n.stringResource
+
+@Composable
+internal fun StorageStep(
+    storagePref: Preference<String>,
+) {
+    val context = LocalContext.current
+    val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
+
+    Column(
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        Text(stringResource(MR.strings.onboarding_storage_info))
+
+        Button(
+            onClick = {
+                try {
+                    pickStorageLocation.launch(null)
+                } catch (e: ActivityNotFoundException) {
+                    context.toast(MR.strings.file_picker_error)
+                }
+            },
+        ) {
+            Text(SettingsDataScreen.storageLocationText(storagePref))
+        }
+    }
+}

+ 40 - 0
app/src/main/java/eu/kanade/presentation/more/onboarding/ThemeStep.kt

@@ -0,0 +1,40 @@
+package eu.kanade.presentation.more.onboarding
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import eu.kanade.domain.ui.UiPreferences
+import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
+import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
+import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
+import tachiyomi.presentation.core.util.collectAsState
+
+@Composable
+internal fun ThemeStep(
+    uiPreferences: UiPreferences,
+) {
+    val themeModePref = uiPreferences.themeMode()
+    val themeMode by themeModePref.collectAsState()
+
+    val appThemePref = uiPreferences.appTheme()
+    val appTheme by appThemePref.collectAsState()
+
+    val amoledPref = uiPreferences.themeDarkAmoled()
+    val amoled by amoledPref.collectAsState()
+
+    Column {
+        AppThemeModePreferenceWidget(
+            value = themeMode,
+            onItemClick = {
+                themeModePref.set(it)
+                setAppCompatDelegateThemeMode(it)
+            },
+        )
+
+        AppThemePreferenceWidget(
+            value = appTheme,
+            amoled = amoled,
+            onItemClick = { appThemePref.set(it) },
+        )
+    }
+}

+ 5 - 0
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt

@@ -43,6 +43,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
 import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
 import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
+import eu.kanade.tachiyomi.ui.more.OnboardingScreen
 import eu.kanade.tachiyomi.util.CrashLogUtil
 import eu.kanade.tachiyomi.util.CrashLogUtil
 import eu.kanade.tachiyomi.util.system.isPreviewBuildType
 import eu.kanade.tachiyomi.util.system.isPreviewBuildType
 import eu.kanade.tachiyomi.util.system.isReleaseBuildType
 import eu.kanade.tachiyomi.util.system.isReleaseBuildType
@@ -110,6 +111,10 @@ object SettingsAdvancedScreen : SearchableSettings {
                         title = stringResource(MR.strings.pref_debug_info),
                         title = stringResource(MR.strings.pref_debug_info),
                         onClick = { navigator.push(DebugInfoScreen()) },
                         onClick = { navigator.push(DebugInfoScreen()) },
                     ),
                     ),
+                    Preference.PreferenceItem.TextPreference(
+                        title = stringResource(MR.strings.pref_onboarding_guide),
+                        onClick = { navigator.push(OnboardingScreen()) },
+                    ),
                 ),
                 ),
             )
             )
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

+ 29 - 43
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt

@@ -2,8 +2,8 @@ package eu.kanade.presentation.more.settings.screen
 
 
 import android.app.Activity
 import android.app.Activity
 import android.content.Context
 import android.content.Context
-import android.os.Build
 import androidx.appcompat.app.AppCompatDelegate
 import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.ReadOnlyComposable
@@ -19,13 +19,11 @@ import eu.kanade.domain.ui.model.TabletUiMode
 import eu.kanade.domain.ui.model.ThemeMode
 import eu.kanade.domain.ui.model.ThemeMode
 import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
 import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
 import eu.kanade.presentation.more.settings.Preference
 import eu.kanade.presentation.more.settings.Preference
+import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
 import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
 import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.system.toast
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.merge
 import org.xmlpull.v1.XmlPullParser
 import org.xmlpull.v1.XmlPullParser
 import tachiyomi.core.i18n.stringResource
 import tachiyomi.core.i18n.stringResource
 import tachiyomi.i18n.MR
 import tachiyomi.i18n.MR
@@ -43,72 +41,59 @@ object SettingsAppearanceScreen : SearchableSettings {
 
 
     @Composable
     @Composable
     override fun getPreferences(): List<Preference> {
     override fun getPreferences(): List<Preference> {
-        val context = LocalContext.current
         val uiPreferences = remember { Injekt.get<UiPreferences>() }
         val uiPreferences = remember { Injekt.get<UiPreferences>() }
 
 
         return listOf(
         return listOf(
-            getThemeGroup(context = context, uiPreferences = uiPreferences),
-            getDisplayGroup(context = context, uiPreferences = uiPreferences),
+            getThemeGroup(uiPreferences = uiPreferences),
+            getDisplayGroup(uiPreferences = uiPreferences),
         )
         )
     }
     }
 
 
     @Composable
     @Composable
     private fun getThemeGroup(
     private fun getThemeGroup(
-        context: Context,
         uiPreferences: UiPreferences,
         uiPreferences: UiPreferences,
     ): Preference.PreferenceGroup {
     ): Preference.PreferenceGroup {
+        val context = LocalContext.current
+
         val themeModePref = uiPreferences.themeMode()
         val themeModePref = uiPreferences.themeMode()
         val themeMode by themeModePref.collectAsState()
         val themeMode by themeModePref.collectAsState()
 
 
         val appThemePref = uiPreferences.appTheme()
         val appThemePref = uiPreferences.appTheme()
+        val appTheme by appThemePref.collectAsState()
 
 
         val amoledPref = uiPreferences.themeDarkAmoled()
         val amoledPref = uiPreferences.themeDarkAmoled()
         val amoled by amoledPref.collectAsState()
         val amoled by amoledPref.collectAsState()
 
 
-        LaunchedEffect(themeMode) {
-            setAppCompatDelegateThemeMode(themeMode)
-        }
-
-        LaunchedEffect(Unit) {
-            merge(appThemePref.changes(), amoledPref.changes())
-                .drop(2)
-                .collectLatest { (context as? Activity)?.let { ActivityCompat.recreate(it) } }
-        }
-
         return Preference.PreferenceGroup(
         return Preference.PreferenceGroup(
             title = stringResource(MR.strings.pref_category_theme),
             title = stringResource(MR.strings.pref_category_theme),
             preferenceItems = listOf(
             preferenceItems = listOf(
-                Preference.PreferenceItem.ListPreference(
-                    pref = themeModePref,
-                    title = stringResource(MR.strings.pref_theme_mode),
-                    entries = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                        mapOf(
-                            ThemeMode.SYSTEM to stringResource(MR.strings.theme_system),
-                            ThemeMode.LIGHT to stringResource(MR.strings.theme_light),
-                            ThemeMode.DARK to stringResource(MR.strings.theme_dark),
-                        )
-                    } else {
-                        mapOf(
-                            ThemeMode.LIGHT to stringResource(MR.strings.theme_light),
-                            ThemeMode.DARK to stringResource(MR.strings.theme_dark),
-                        )
-                    },
-                ),
                 Preference.PreferenceItem.CustomPreference(
                 Preference.PreferenceItem.CustomPreference(
                     title = stringResource(MR.strings.pref_app_theme),
                     title = stringResource(MR.strings.pref_app_theme),
-                ) { item ->
-                    val value by appThemePref.collectAsState()
-                    AppThemePreferenceWidget(
-                        title = item.title,
-                        value = value,
-                        amoled = amoled,
-                        onItemClick = { appThemePref.set(it) },
-                    )
+                ) {
+                    Column {
+                        AppThemeModePreferenceWidget(
+                            value = themeMode,
+                            onItemClick = {
+                                themeModePref.set(it)
+                                setAppCompatDelegateThemeMode(it)
+                            },
+                        )
+
+                        AppThemePreferenceWidget(
+                            value = appTheme,
+                            amoled = amoled,
+                            onItemClick = { appThemePref.set(it) },
+                        )
+                    }
                 },
                 },
                 Preference.PreferenceItem.SwitchPreference(
                 Preference.PreferenceItem.SwitchPreference(
                     pref = amoledPref,
                     pref = amoledPref,
                     title = stringResource(MR.strings.pref_dark_theme_pure_black),
                     title = stringResource(MR.strings.pref_dark_theme_pure_black),
                     enabled = themeMode != ThemeMode.LIGHT,
                     enabled = themeMode != ThemeMode.LIGHT,
+                    onValueChanged = {
+                        (context as? Activity)?.let { ActivityCompat.recreate(it) }
+                        true
+                    },
                 ),
                 ),
             ),
             ),
         )
         )
@@ -116,9 +101,10 @@ object SettingsAppearanceScreen : SearchableSettings {
 
 
     @Composable
     @Composable
     private fun getDisplayGroup(
     private fun getDisplayGroup(
-        context: Context,
         uiPreferences: UiPreferences,
         uiPreferences: UiPreferences,
     ): Preference.PreferenceGroup {
     ): Preference.PreferenceGroup {
+        val context = LocalContext.current
+
         val langs = remember { getLangs(context) }
         val langs = remember { getLangs(context) }
         var currentLanguage by remember {
         var currentLanguage by remember {
             mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")
             mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")

+ 28 - 10
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt

@@ -7,6 +7,7 @@ import android.net.Uri
 import android.os.Environment
 import android.os.Environment
 import android.text.format.Formatter
 import android.text.format.Formatter
 import android.widget.Toast
 import android.widget.Toast
+import androidx.activity.compose.ManagedActivityResultLauncher
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
@@ -80,13 +81,12 @@ object SettingsDataScreen : SearchableSettings {
     }
     }
 
 
     @Composable
     @Composable
-    private fun getStorageLocationPref(
-        storagePreferences: StoragePreferences,
-    ): Preference.PreferenceItem.TextPreference {
+    fun storageLocationPicker(
+        storageDirPref: tachiyomi.core.preference.Preference<String>,
+    ): ManagedActivityResultLauncher<Uri?, Uri?> {
         val context = LocalContext.current
         val context = LocalContext.current
-        val storageDirPref = storagePreferences.baseStorageDirectory()
-        val storageDir by storageDirPref.collectAsState()
-        val pickStorageLocation = rememberLauncherForActivityResult(
+
+        return rememberLauncherForActivityResult(
             contract = ActivityResultContracts.OpenDocumentTree(),
             contract = ActivityResultContracts.OpenDocumentTree(),
         ) { uri ->
         ) { uri ->
             if (uri != null) {
             if (uri != null) {
@@ -101,13 +101,31 @@ object SettingsDataScreen : SearchableSettings {
                 Injekt.get<DownloadCache>().invalidateCache()
                 Injekt.get<DownloadCache>().invalidateCache()
             }
             }
         }
         }
+    }
+
+    @Composable
+    fun storageLocationText(
+        storageDirPref: tachiyomi.core.preference.Preference<String>,
+    ): String {
+        val context = LocalContext.current
+        val storageDir by storageDirPref.collectAsState()
+
+        return remember(storageDir) {
+            val file = UniFile.fromUri(context, storageDir.toUri())
+            file?.filePath ?: file?.uri?.toString()
+        } ?: stringResource(MR.strings.invalid_location, storageDir)
+    }
+
+    @Composable
+    private fun getStorageLocationPref(
+        storagePreferences: StoragePreferences,
+    ): Preference.PreferenceItem.TextPreference {
+        val context = LocalContext.current
+        val pickStorageLocation = storageLocationPicker(storagePreferences.baseStorageDirectory())
 
 
         return Preference.PreferenceItem.TextPreference(
         return Preference.PreferenceItem.TextPreference(
             title = stringResource(MR.strings.pref_storage_location),
             title = stringResource(MR.strings.pref_storage_location),
-            subtitle = remember(storageDir) {
-                val file = UniFile.fromUri(context, storageDir.toUri())
-                file?.filePath ?: file?.uri?.toString()
-            } ?: stringResource(MR.strings.invalid_location, storageDir),
+            subtitle = storageLocationText(storagePreferences.baseStorageDirectory()),
             onClick = {
             onClick = {
                 try {
                 try {
                     pickStorageLocation.launch(null)
                     pickStorageLocation.launch(null)

+ 56 - 0
app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemeModePreferenceWidget.kt

@@ -0,0 +1,56 @@
+package eu.kanade.presentation.more.settings.widget
+
+import android.os.Build
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MultiChoiceSegmentedButtonRow
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import eu.kanade.domain.ui.model.ThemeMode
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.i18n.stringResource
+
+private val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+    mapOf(
+        ThemeMode.SYSTEM to MR.strings.theme_system,
+        ThemeMode.LIGHT to MR.strings.theme_light,
+        ThemeMode.DARK to MR.strings.theme_dark,
+    )
+} else {
+    mapOf(
+        ThemeMode.LIGHT to MR.strings.theme_light,
+        ThemeMode.DARK to MR.strings.theme_dark,
+    )
+}
+
+@Composable
+internal fun AppThemeModePreferenceWidget(
+    value: ThemeMode,
+    onItemClick: (ThemeMode) -> Unit,
+) {
+    BasePreferenceWidget(
+        subcomponent = {
+            MultiChoiceSegmentedButtonRow(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(horizontal = PrefsHorizontalPadding),
+            ) {
+                options.onEachIndexed { index, (mode, labelRes) ->
+                    SegmentedButton(
+                        checked = mode == value,
+                        onCheckedChange = { onItemClick(mode) },
+                        shape = SegmentedButtonDefaults.itemShape(
+                            index,
+                            options.size,
+                        ),
+                    ) {
+                        Text(stringResource(labelRes))
+                    }
+                }
+            }
+        },
+    )
+}

+ 8 - 3
app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt

@@ -1,5 +1,6 @@
 package eu.kanade.presentation.more.settings.widget
 package eu.kanade.presentation.more.settings.widget
 
 
+import android.app.Activity
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clickable
@@ -36,9 +37,11 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.PreviewLightDark
 import androidx.compose.ui.tooling.preview.PreviewLightDark
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import androidx.core.app.ActivityCompat
 import eu.kanade.domain.ui.model.AppTheme
 import eu.kanade.domain.ui.model.AppTheme
 import eu.kanade.presentation.manga.components.MangaCover
 import eu.kanade.presentation.manga.components.MangaCover
 import eu.kanade.presentation.theme.TachiyomiTheme
 import eu.kanade.presentation.theme.TachiyomiTheme
@@ -51,13 +54,11 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
 
 
 @Composable
 @Composable
 internal fun AppThemePreferenceWidget(
 internal fun AppThemePreferenceWidget(
-    title: String,
     value: AppTheme,
     value: AppTheme,
     amoled: Boolean,
     amoled: Boolean,
     onItemClick: (AppTheme) -> Unit,
     onItemClick: (AppTheme) -> Unit,
 ) {
 ) {
     BasePreferenceWidget(
     BasePreferenceWidget(
-        title = title,
         subcomponent = {
         subcomponent = {
             AppThemesList(
             AppThemesList(
                 currentTheme = value,
                 currentTheme = value,
@@ -74,6 +75,7 @@ private fun AppThemesList(
     amoled: Boolean,
     amoled: Boolean,
     onItemClick: (AppTheme) -> Unit,
     onItemClick: (AppTheme) -> Unit,
 ) {
 ) {
+    val context = LocalContext.current
     val appThemes = remember {
     val appThemes = remember {
         AppTheme.entries
         AppTheme.entries
             .filterNot { it.titleRes == null || (it == AppTheme.MONET && !DeviceUtil.isDynamicColorAvailable) }
             .filterNot { it.titleRes == null || (it == AppTheme.MONET && !DeviceUtil.isDynamicColorAvailable) }
@@ -97,7 +99,10 @@ private fun AppThemesList(
                 ) {
                 ) {
                     AppThemePreviewItem(
                     AppThemePreviewItem(
                         selected = currentTheme == appTheme,
                         selected = currentTheme == appTheme,
-                        onClick = { onItemClick(appTheme) },
+                        onClick = {
+                            onItemClick(appTheme)
+                            (context as? Activity)?.let { ActivityCompat.recreate(it) }
+                        },
                     )
                     )
                 }
                 }
 
 

+ 6 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt

@@ -126,12 +126,12 @@ object HomeScreen : Screen() {
                                 materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
                                 materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
                                     materialFadeThroughOut(durationMillis = TabFadeDuration)
                                     materialFadeThroughOut(durationMillis = TabFadeDuration)
                             },
                             },
-                            content = {
-                                tabNavigator.saveableState(key = "currentTab", it) {
-                                    it.Content()
-                                }
-                            },
-                        )
+                            label = "tabContent",
+                        ) {
+                            tabNavigator.saveableState(key = "currentTab", it) {
+                                it.Content()
+                            }
+                        }
                     }
                     }
                 }
                 }
             }
             }

+ 13 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -73,6 +73,7 @@ import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen
 import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
 import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
+import eu.kanade.tachiyomi.ui.more.OnboardingScreen
 import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
 import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
 import eu.kanade.tachiyomi.util.system.openInBrowser
 import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -251,6 +252,7 @@ class MainActivity : BaseActivity() {
                 HandleOnNewIntent(context = context, navigator = navigator)
                 HandleOnNewIntent(context = context, navigator = navigator)
 
 
                 CheckForUpdates()
                 CheckForUpdates()
+                ShowOnboarding()
             }
             }
 
 
             var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
             var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
@@ -342,6 +344,17 @@ class MainActivity : BaseActivity() {
         }
         }
     }
     }
 
 
+    @Composable
+    private fun ShowOnboarding() {
+        val navigator = LocalNavigator.currentOrThrow
+
+        LaunchedEffect(Unit) {
+            if (!preferences.shownOnboardingFlow().get()) {
+                navigator.push(OnboardingScreen())
+            }
+        }
+    }
+
     /**
     /**
      * Sets custom splash screen exit animation on devices prior to Android 12.
      * Sets custom splash screen exit animation on devices prior to Android 12.
      *
      *

+ 34 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt

@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.ui.more
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.domain.ui.UiPreferences
+import eu.kanade.presentation.more.onboarding.OnboardingScreen
+import eu.kanade.presentation.util.Screen
+import tachiyomi.domain.storage.service.StoragePreferences
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class OnboardingScreen : Screen() {
+
+    @Composable
+    override fun Content() {
+        val navigator = LocalNavigator.currentOrThrow
+
+        val basePreferences = remember { Injekt.get<BasePreferences>() }
+        val storagePreferences = remember { Injekt.get<StoragePreferences>() }
+        val uiPreferences = remember { Injekt.get<UiPreferences>() }
+
+        OnboardingScreen(
+            storagePreferences = storagePreferences,
+            uiPreferences = uiPreferences,
+            onComplete = {
+                basePreferences.shownOnboardingFlow().set(true)
+                navigator.pop()
+            },
+        )
+    }
+}

+ 1 - 1
core/src/main/java/tachiyomi/core/preference/InMemoryPreferenceStore.kt

@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.stateIn
  * Local-copy implementation of PreferenceStore mostly for test and preview purposes
  * Local-copy implementation of PreferenceStore mostly for test and preview purposes
  */
  */
 class InMemoryPreferenceStore(
 class InMemoryPreferenceStore(
-    private val initialPreferences: Sequence<InMemoryPreference<*>> = sequenceOf(),
+    initialPreferences: Sequence<InMemoryPreference<*>> = sequenceOf(),
 ) : PreferenceStore {
 ) : PreferenceStore {
 
 
     private val preferences: Map<String, Preference<*>> =
     private val preferences: Map<String, Preference<*>> =

+ 12 - 4
i18n/src/commonMain/resources/MR/base/strings.xml

@@ -173,6 +173,15 @@
     <!-- Shortcuts-->
     <!-- Shortcuts-->
     <string name="app_not_available">App not available</string>
     <string name="app_not_available">App not available</string>
 
 
+    <!-- Onboarding -->
+    <string name="pref_onboarding_guide">Onboarding guide</string>
+    <string name="onboarding_heading">Welcome!</string>
+    <string name="onboarding_description">Let\'s set some things up first. You can always change these in the settings later too.</string>
+    <string name="onboarding_action_next">Next</string>
+    <string name="onboarding_action_finish">Get started</string>
+    <string name="onboarding_action_skip">Skip</string>
+    <string name="onboarding_storage_info">Select a storage location where chapter downloads, backups, and local source content will be stored.</string>
+
     <!-- Preferences -->
     <!-- Preferences -->
       <!-- Subsections -->
       <!-- Subsections -->
     <string name="pref_category_general">General</string>
     <string name="pref_category_general">General</string>
@@ -196,11 +205,10 @@
 
 
       <!-- General section -->
       <!-- General section -->
     <string name="pref_category_theme">Theme</string>
     <string name="pref_category_theme">Theme</string>
-    <string name="pref_theme_mode">Dark mode</string>
-    <string name="theme_system">Follow system</string>
-    <string name="theme_light">Off</string>
-    <string name="theme_dark">On</string>
     <string name="pref_app_theme">App theme</string>
     <string name="pref_app_theme">App theme</string>
+    <string name="theme_system">System</string>
+    <string name="theme_light">Light</string>
+    <string name="theme_dark">Dark</string>
     <string name="theme_monet">Dynamic</string>
     <string name="theme_monet">Dynamic</string>
     <string name="theme_greenapple">Green Apple</string>
     <string name="theme_greenapple">Green Apple</string>
     <string name="theme_lavender">Lavender</string>
     <string name="theme_lavender">Lavender</string>