From f28ed2cfb375a53d7c591bc4a8cf3fd0c49363d1 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 31 May 2024 09:51:28 +0700 Subject: [PATCH] feat: WIP Compose Settings --- .../onboarding/steps/StorageStep.kt | 18 +-- .../settings/ComposableSettings.kt | 32 ++++ .../settings/SettingsCommonWidget.kt | 92 +++++++++++ .../settings/SettingsDataScreen.kt | 145 ++++++++++++++++++ 4 files changed, 270 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt create mode 100644 app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt create mode 100644 app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt diff --git a/app/src/main/java/dev/yokai/presentation/onboarding/steps/StorageStep.kt b/app/src/main/java/dev/yokai/presentation/onboarding/steps/StorageStep.kt index 69f8204838..f2b6daf541 100644 --- a/app/src/main/java/dev/yokai/presentation/onboarding/steps/StorageStep.kt +++ b/app/src/main/java/dev/yokai/presentation/onboarding/steps/StorageStep.kt @@ -28,6 +28,7 @@ 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.settings.storageLocationText import dev.yokai.presentation.theme.Size import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.preference.Preference @@ -122,20 +123,3 @@ fun storageLocationPicker( } } } - -@Composable -fun storageLocationText( - storageDirPref: Preference, -): 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) -} diff --git a/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt b/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt new file mode 100644 index 0000000000..3a75470698 --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt @@ -0,0 +1,32 @@ +package dev.yokai.presentation.settings + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource +import dev.yokai.presentation.component.preference.Preference + +interface ComposableSettings { + fun getOnBackPress(): () -> Unit = {} + + @Composable + @ReadOnlyComposable + @StringRes + fun getTitleRes(): Int + + @Composable + fun getPreferences(): List + + @Composable + fun RowScope.AppBarAction() {} + + @Composable + fun Content() { + SettingsScaffold( + title = stringResource(getTitleRes()), + itemsProvider = { getPreferences() }, + onBackPress = getOnBackPress(), + ) + } +} diff --git a/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt b/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt new file mode 100644 index 0000000000..48ec56a0dc --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt @@ -0,0 +1,92 @@ +package dev.yokai.presentation.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import dev.yokai.presentation.AppBarType +import dev.yokai.presentation.YokaiScaffold +import dev.yokai.presentation.component.Gap +import dev.yokai.presentation.component.preference.Preference +import dev.yokai.presentation.component.preference.PreferenceItem +import dev.yokai.presentation.component.preference.widget.PreferenceGroupHeader +import eu.kanade.tachiyomi.core.preference.collectAsState +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import uy.kohesive.injekt.injectLazy + +@Composable +fun SettingsScaffold( + title: String, + appBarType: AppBarType? = null, + onBackPress: (() -> Unit)? = null, + itemsProvider: @Composable () -> List, +) { + val preferences: PreferencesHelper by injectLazy() + val useLargeAppBar by preferences.useLargeToolbar().collectAsState() + val listState = rememberLazyListState() + + YokaiScaffold( + onNavigationIconClicked = onBackPress ?: {}, + title = title, + appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL, + scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + state = rememberTopAppBarState(), + canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 }, + ), + ) { innerPadding -> + PreferenceScreen( + items = itemsProvider(), + listState = listState, + contentPadding = innerPadding, + ) + } +} + +@Composable +fun PreferenceScreen( + items: List, + listState: LazyListState, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), +) { + LazyColumn( + modifier = modifier, + contentPadding = contentPadding, + state = listState + ) { + val highlightKey = null as String? // TODO + items.fastForEachIndexed { i, preference -> + when (preference) { + is Preference.PreferenceGroup -> { + if (!preference.enabled) return@fastForEachIndexed + + item { + Column { + PreferenceGroupHeader(title = preference.title) + } + } + items(preference.preferenceItems) { item -> + PreferenceItem(item = item, highlightKey = highlightKey) + } + item { + if (i < items.lastIndex) { + Gap(padding = 12.dp) + } + } + } + is Preference.PreferenceItem<*> -> item { + PreferenceItem(item = preference, highlightKey = highlightKey) + } + } + } + } +} diff --git a/app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt b/app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt new file mode 100644 index 0000000000..b5ee11f13a --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt @@ -0,0 +1,145 @@ +package dev.yokai.presentation.settings + +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.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.net.toUri +import com.google.common.collect.ImmutableList +import com.hippo.unifile.UniFile +import dev.yokai.domain.storage.StoragePreferences +import dev.yokai.presentation.component.preference.Preference +import dev.yokai.presentation.component.preference.widget.BasePreferenceWidget +import dev.yokai.presentation.component.preference.widget.PrefsHorizontalPadding +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.toast +import uy.kohesive.injekt.injectLazy + +object SettingsDataScreen : ComposableSettings { + @Composable + override fun getTitleRes(): Int = R.string.data_and_storage + + @Composable + override fun getPreferences(): List { + val storagePreferences: StoragePreferences by injectLazy() + val preferences: PreferencesHelper by injectLazy() + + return ImmutableList.of( + getStorageLocationPreference(storagePreferences = storagePreferences), + getBackupAndRestoreGroup(preferences = preferences), + ) + } + + @Composable + private fun storageLocationPicker( + baseStorageDirectory: eu.kanade.tachiyomi.core.preference.Preference, + ): ManagedActivityResultLauncher { + 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 { + baseStorageDirectory.set(it.uri.toString()) + } + } + } + } + + @Composable + private fun getStorageLocationPreference(storagePreferences: StoragePreferences): Preference.PreferenceItem.TextPreference { + val context = LocalContext.current + val pickStoragePicker = storageLocationPicker(storagePreferences.baseStorageDirectory()) + + return Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.storage_location), + subtitle = storageLocationText(storagePreferences.baseStorageDirectory()), + onClick = { + try { + pickStoragePicker.launch(null) + } catch (e: ActivityNotFoundException) { + context.toast(R.string.file_picker_error) + } + } + ) + } + + @Composable + private fun getBackupAndRestoreGroup(preferences: PreferencesHelper): Preference.PreferenceGroup { + return Preference.PreferenceGroup( + title = stringResource(R.string.backup_and_restore), + preferenceItems = ImmutableList.of( + Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.backup_and_restore), + ) { + BasePreferenceWidget( + subcomponent = { + MultiChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Min) + .padding(horizontal = PrefsHorizontalPadding), + ) { + SegmentedButton( + modifier = Modifier.fillMaxHeight(), + checked = false, + onCheckedChange = {}, + shape = SegmentedButtonDefaults.itemShape(0, 2), + ) { + Text(stringResource(R.string.create_backup)) + } + SegmentedButton( + modifier = Modifier.fillMaxHeight(), + checked = false, + onCheckedChange = {}, + shape = SegmentedButtonDefaults.itemShape(1, 2), + ) { + Text(stringResource(R.string.restore_backup)) + } + } + }, + ) + }, + ), + ) + } +} + +@Composable +fun storageLocationText( + storageDirPref: eu.kanade.tachiyomi.core.preference.Preference, +): 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) +}