feat: Finish onboarding screen

Co-Authored-By: nonproto <nonproto@users.noreply.github.com>
This commit is contained in:
Ahmad Ansori Palembani 2024-05-27 06:20:32 +07:00
parent 4060278332
commit 9046b343de
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
6 changed files with 388 additions and 3 deletions

View 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))
}

View file

@ -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

View file

@ -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),
)
}
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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-->