mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: Finish onboarding screen
Co-Authored-By: nonproto <nonproto@users.noreply.github.com>
This commit is contained in:
parent
4060278332
commit
9046b343de
6 changed files with 388 additions and 3 deletions
27
app/src/main/java/dev/yokai/presentation/component/Gap.kt
Normal file
27
app/src/main/java/dev/yokai/presentation/component/Gap.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package dev.yokai.presentation.component
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
fun RowScope.Gap(width: Dp, modifier: Modifier = Modifier) {
|
||||
Spacer(modifier = modifier.width(width))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.Gap(height: Dp, modifier: Modifier = Modifier) {
|
||||
Spacer(modifier = modifier.height(height))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.Gap(padding: Dp, modifier: Modifier = Modifier) {
|
||||
Spacer(modifier = modifier.size(padding))
|
||||
}
|
|
@ -20,6 +20,8 @@ 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.PermissionStep
|
||||
import dev.yokai.presentation.onboarding.steps.StorageStep
|
||||
import dev.yokai.presentation.onboarding.steps.ThemeStep
|
||||
import dev.yokai.presentation.theme.Size
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@ -36,8 +38,8 @@ fun OnboardingScreen(
|
|||
val steps = remember {
|
||||
listOf(
|
||||
ThemeStep(),
|
||||
ThemeStep(),
|
||||
ThemeStep(),
|
||||
StorageStep(),
|
||||
PermissionStep(),
|
||||
)
|
||||
}
|
||||
val isLastStep = currentStep == steps.lastIndex
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
package dev.yokai.presentation.onboarding.steps
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import dev.yokai.presentation.component.Gap
|
||||
import dev.yokai.presentation.theme.Size
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
internal class PermissionStep : OnboardingStep {
|
||||
|
||||
private var installGranted by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = installGranted
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
var notificationGranted by remember {
|
||||
mutableStateOf(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var batteryGranted by remember {
|
||||
mutableStateOf(
|
||||
context
|
||||
.getSystemService<PowerManager>()!!
|
||||
.isIgnoringBatteryOptimizations(context.packageName)
|
||||
)
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
installGranted =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Settings.Secure.getInt(
|
||||
context.contentResolver,
|
||||
Settings.Secure.INSTALL_NON_MARKET_APPS
|
||||
) != 0
|
||||
}
|
||||
batteryGranted =
|
||||
context
|
||||
.getSystemService<PowerManager>()!!
|
||||
.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = Size.medium),
|
||||
) {
|
||||
SectionHeader(stringResource(R.string.onboarding_permission_type_required))
|
||||
|
||||
PermissionItem(
|
||||
title = stringResource(R.string.onboarding_permission_install_apps),
|
||||
subtitle = stringResource(R.string.onboarding_permission_install_apps_description),
|
||||
granted = installGranted,
|
||||
onButtonClick = {
|
||||
val intent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
} else {
|
||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
|
||||
Gap(Size.medium)
|
||||
|
||||
SectionHeader(stringResource(R.string.onboarding_permission_type_optional))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permissionRequester =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
onResult = { bool -> notificationGranted = bool },
|
||||
)
|
||||
PermissionItem(
|
||||
title = stringResource(R.string.onboarding_permission_notifications),
|
||||
subtitle =
|
||||
stringResource(R.string.onboarding_permission_notifications_description),
|
||||
granted = notificationGranted,
|
||||
onButtonClick = {
|
||||
permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PermissionItem(
|
||||
title = stringResource(R.string.onboarding_permission_ignore_battery_opts),
|
||||
subtitle =
|
||||
stringResource(R.string.onboarding_permission_ignore_battery_opts_description),
|
||||
granted = batteryGranted,
|
||||
onButtonClick = {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent =
|
||||
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = modifier.padding(horizontal = Size.medium),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionItem(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
granted: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onButtonClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
trailingContent = {
|
||||
OutlinedButton(
|
||||
enabled = !granted,
|
||||
onClick = onButtonClick,
|
||||
) {
|
||||
if (granted) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(R.string.onboarding_permission_action_grant))
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package dev.yokai.presentation.onboarding.steps
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import dev.yokai.domain.storage.StoragePreferences
|
||||
import dev.yokai.presentation.theme.Size
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class StorageStep : OnboardingStep {
|
||||
|
||||
private val storagePref: StoragePreferences by injectLazy()
|
||||
|
||||
private var _isComplete by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = _isComplete
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val handler = LocalUriHandler.current
|
||||
|
||||
val pickStorageLocation = storageLocationPicker(storagePref.baseStorageDirectory())
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(Size.medium).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(Size.small),
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.onboarding_storage_info,
|
||||
stringResource(R.string.app_name),
|
||||
storageLocationText(storagePref.baseStorageDirectory()),
|
||||
),
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(R.string.file_picker_error)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.onboarding_storage_action_select))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = Size.small),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.onboarding_storage_help_info))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
handler.openUri(
|
||||
"https://mihon.app/docs/faq/storage#migrating-from-tachiyomi-v0-14-x-or-earlier"
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.onboarding_storage_help_action))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
storagePref.baseStorageDirectory().changes().collectLatest {
|
||||
_isComplete = storagePref.baseStorageDirectory().isSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun storageLocationPicker(
|
||||
storageDirPref: Preference<String>,
|
||||
): ManagedActivityResultLauncher<Uri?, Uri?> {
|
||||
val context = LocalContext.current
|
||||
|
||||
return rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree(),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
UniFile.fromUri(context, uri)?.let { storageDirPref.set(it.uri.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun storageLocationText(
|
||||
storageDirPref: Preference<String>,
|
||||
): String {
|
||||
val context = LocalContext.current
|
||||
val storageDir by storageDirPref.collectAsState()
|
||||
|
||||
if (storageDir == storageDirPref.defaultValue()) {
|
||||
return stringResource(R.string.no_location_set)
|
||||
}
|
||||
|
||||
return remember(storageDir) {
|
||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||
file?.filePath
|
||||
} ?: stringResource(R.string.invalid_location, storageDir)
|
||||
}
|
|
@ -35,7 +35,7 @@ import eu.kanade.tachiyomi.util.system.Themes
|
|||
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ThemeStep : OnboardingStep {
|
||||
internal class ThemeStep : OnboardingStep {
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
|
|
@ -11,6 +11,22 @@
|
|||
<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>
|
||||
<string name="onboarding_storage_info">Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s</string>
|
||||
<string name="onboarding_storage_action_select">Select a folder</string>
|
||||
<string name="onboarding_storage_help_info">Updating from an older version and not sure what to select? Refer to the Tachiyomi upgrade section on the Mihon storage guide for more information.</string>
|
||||
<string name="onboarding_storage_help_action">Storage guide</string>
|
||||
<string name="onboarding_permission_type_required">Required</string>
|
||||
<string name="onboarding_permission_type_optional">Optional but recommended</string>
|
||||
<string name="onboarding_permission_install_apps">Install apps permission</string>
|
||||
<string name="onboarding_permission_install_apps_description">To install the app on updates.</string>
|
||||
<string name="onboarding_permission_notifications">Notification permission</string>
|
||||
<string name="onboarding_permission_notifications_description">Get notified for library updates and more.</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Background battery usage</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Avoid interruptions to long-running library updates, downloads, and backup restores.</string>
|
||||
<string name="onboarding_permission_action_grant">Grant</string>
|
||||
|
||||
<string name="no_location_set">No storage location set</string>
|
||||
<string name="invalid_location">Invalid location: %s</string>
|
||||
|
||||
<!--Models-->
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue