diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 7f7cfc74..70d60544 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -285,7 +285,7 @@ class AppSettingsActivity: AppCompatActivity() { private fun resetHints() { settings.remove(BatteryOptimizationsFragment.Model.HINT_BATTERY_OPTIMIZATIONS) settings.remove(BatteryOptimizationsFragment.Model.HINT_AUTOSTART_PERMISSION) - settings.remove(TasksFragment.Model.HINT_OPENTASKS_NOT_INSTALLED) + settings.remove(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) settings.remove(OpenSourceFragment.Model.SETTING_NEXT_DONATION_POPUP) Snackbar.make(requireView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt index 30428918..23705292 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt @@ -5,19 +5,23 @@ package at.bitfire.davdroid.ui import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import com.google.accompanist.themeadapter.material.MdcTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class TasksActivity: AppCompatActivity() { + val model: TasksModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (savedInstanceState == null) - supportFragmentManager.beginTransaction() - .add(android.R.id.content, TasksFragment()) - .commit() + setContent { + MdcTheme { + TasksCard(model) + } + } } - -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt index 04dc578c..800e5f11 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksFragment.kt @@ -9,191 +9,305 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.annotation.AnyThread -import androidx.databinding.ObservableBoolean -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Checkbox +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.unit.dp +import androidx.core.text.HtmlCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.PackageChangedReceiver import at.bitfire.davdroid.R -import at.bitfire.davdroid.databinding.ActivityTasksBinding import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString +import at.bitfire.davdroid.ui.widget.CardWithImage +import at.bitfire.davdroid.ui.widget.RadioWithSwitch import at.bitfire.ical4android.TaskProvider.ProviderName -import com.google.android.material.snackbar.Snackbar -import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject -@AndroidEntryPoint -class TasksFragment: Fragment() { +@HiltViewModel +class TasksModel @Inject constructor( + application: Application, + val settings: SettingsManager +) : AndroidViewModel(application), SettingsManager.OnChangeListener { - private var _binding: ActivityTasksBinding? = null - private val binding get() = _binding!! - val model by viewModels() + companion object { + /** + * Whether this fragment (which asks for OpenTasks installation) shall be shown. + * If this setting is true or null/not set, the notice shall be shown. Only if this + * setting is false, the notice shall not be shown. + */ + const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled" - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = ActivityTasksBinding.inflate(inflater, container, false) - binding.lifecycleOwner = viewLifecycleOwner - binding.model = model - - model.openTasksRequested.observe(viewLifecycleOwner) { shallBeInstalled -> - if (shallBeInstalled && model.openTasksInstalled.value == false) { - // uncheck switch for the moment (until the app is installed) - model.openTasksRequested.value = false - installApp(ProviderName.OpenTasks.packageName) - } - } - model.openTasksSelected.observe(viewLifecycleOwner) { selected -> - if (selected && model.currentProvider.value != ProviderName.OpenTasks) - model.selectPreferredProvider(ProviderName.OpenTasks) - } - - model.tasksOrgRequested.observe(viewLifecycleOwner) { shallBeInstalled -> - if (shallBeInstalled && model.tasksOrgInstalled.value == false) { - model.tasksOrgRequested.value = false - installApp(ProviderName.TasksOrg.packageName) - } - } - model.tasksOrgSelected.observe(viewLifecycleOwner) { selected -> - if (selected && model.currentProvider.value != ProviderName.TasksOrg) - model.selectPreferredProvider(ProviderName.TasksOrg) - } - - - model.jtxRequested.observe(viewLifecycleOwner) { shallBeInstalled -> - if (shallBeInstalled && model.jtxInstalled.value == false) { - model.jtxRequested.value = false - installApp(ProviderName.JtxBoard.packageName) - } - } - model.jtxSelected.observe(viewLifecycleOwner) { selected -> - if (selected && model.currentProvider.value != ProviderName.JtxBoard) - model.selectPreferredProvider(ProviderName.JtxBoard) - } - - binding.infoLeaveUnchecked.text = getString(R.string.intro_leave_unchecked, getString(R.string.app_settings_reset_hints)) - - return binding.root } - override fun onDestroyView() { - super.onDestroyView() - _binding = null + val currentProvider = MutableLiveData() + val openTasksInstalled = MutableLiveData() + val openTasksRequested = MutableLiveData() + val openTasksSelected = MutableLiveData() + val tasksOrgInstalled = MutableLiveData() + val tasksOrgRequested = MutableLiveData() + val tasksOrgSelected = MutableLiveData() + val jtxInstalled = MutableLiveData() + val jtxRequested = MutableLiveData() + val jtxSelected = MutableLiveData() + + private val tasksWatcher = object: PackageChangedReceiver(application) { + override fun onReceive(context: Context?, intent: Intent?) { + checkInstalled() + } } - private fun installApp(packageName: String) { - val uri = Uri.parse("market://details?id=$packageName&referrer=" + - Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID)) - val intent = Intent(Intent.ACTION_VIEW, uri) - if (intent.resolveActivity(requireActivity().packageManager) != null) - startActivity(intent) + val dontShow = MutableLiveData( + settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false + ) + + private val dontShowObserver = Observer { value -> + if (value) + settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false) else - Snackbar.make(binding.frame, R.string.intro_tasks_no_app_store, Snackbar.LENGTH_LONG).show() + settings.remove(HINT_OPENTASKS_NOT_INSTALLED) + } + + init { + checkInstalled() + settings.addOnChangeListener(this) + dontShow.observeForever(dontShowObserver) + } + + override fun onCleared() { + settings.removeOnChangeListener(this) + tasksWatcher.close() + dontShow.removeObserver(dontShowObserver) + } + + @AnyThread + fun checkInstalled() { + val taskProvider = TaskUtils.currentProvider(getApplication()) + currentProvider.postValue(taskProvider) + + val openTasks = isInstalled(ProviderName.OpenTasks.packageName) + openTasksInstalled.postValue(openTasks) + openTasksRequested.postValue(openTasks) + openTasksSelected.postValue(taskProvider == ProviderName.OpenTasks) + + val tasksOrg = isInstalled(ProviderName.TasksOrg.packageName) + tasksOrgInstalled.postValue(tasksOrg) + tasksOrgRequested.postValue(tasksOrg) + tasksOrgSelected.postValue(taskProvider == ProviderName.TasksOrg) + + val jtxBoard = isInstalled(ProviderName.JtxBoard.packageName) + jtxInstalled.postValue(jtxBoard) + jtxRequested.postValue(jtxBoard) + jtxSelected.postValue(taskProvider == ProviderName.JtxBoard) + } + + private fun isInstalled(packageName: String): Boolean = + try { + getApplication().packageManager.getPackageInfo(packageName, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } + + fun selectPreferredProvider(provider: ProviderName) { + // Changes preferred task app setting, so onSettingsChanged() will be called + TaskUtils.setPreferredProvider(getApplication(), provider) } - @HiltViewModel - class Model @Inject constructor( - application: Application, - val settings: SettingsManager - ) : AndroidViewModel(application), SettingsManager.OnChangeListener { + override fun onSettingsChanged() { + checkInstalled() + } - companion object { +} - /** - * Whether this fragment (which asks for OpenTasks installation) shall be shown. - * If this setting is true or null/not set, the notice shall be shown. Only if this - * setting is false, the notice shall not be shown. - */ - const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled" +@OptIn(ExperimentalTextApi::class) +@Composable +fun TasksCard( + model: TasksModel = viewModel() +) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() - } + val snackbarHostState = remember { SnackbarHostState() } - val context: Context get() = getApplication() + val jtxInstalled by model.jtxInstalled.observeAsState(initial = false) + val jtxSelected by model.jtxSelected.observeAsState(initial = false) + val jtxRequested by model.jtxRequested.observeAsState(initial = false) - val currentProvider = MutableLiveData() - val openTasksInstalled = MutableLiveData() - val openTasksRequested = MutableLiveData() - val openTasksSelected = MutableLiveData() - val tasksOrgInstalled = MutableLiveData() - val tasksOrgRequested = MutableLiveData() - val tasksOrgSelected = MutableLiveData() - val jtxInstalled = MutableLiveData() - val jtxRequested = MutableLiveData() - val jtxSelected = MutableLiveData() - val tasksWatcher = object: PackageChangedReceiver(context) { - override fun onReceive(context: Context?, intent: Intent?) { - checkInstalled() + val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(initial = false) + val tasksOrgSelected by model.tasksOrgSelected.observeAsState(initial = false) + val tasksOrgRequested by model.tasksOrgRequested.observeAsState(initial = false) + + val openTasksInstalled by model.openTasksInstalled.observeAsState(initial = false) + val openTasksSelected by model.openTasksSelected.observeAsState(initial = false) + val openTasksRequested by model.openTasksRequested.observeAsState(initial = false) + + val dontShow by model.dontShow.observeAsState(initial = false) + + fun installApp(packageName: String) { + val uri = Uri.parse("market://details?id=$packageName&referrer=" + + Uri.encode("utm_source=" + BuildConfig.APPLICATION_ID)) + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(context.packageManager) != null) + context.startActivity(intent) + else + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = context.getString(R.string.intro_tasks_no_app_store), + duration = SnackbarDuration.Long + ) } - } + } - val dontShow = object: ObservableBoolean() { - override fun get() = settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false - override fun set(dontShowAgain: Boolean) { - if (dontShowAgain) - settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false) - else - settings.remove(HINT_OPENTASKS_NOT_INSTALLED) - notifyChange() - } - } + fun onProviderSelected(provider: ProviderName) { + if (model.currentProvider.value != provider) + model.selectPreferredProvider(provider) + } - init { - checkInstalled() - settings.addOnChangeListener(this) - } + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxHeight() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + CardWithImage( + image = painterResource(R.drawable.intro_tasks), + title = stringResource(R.string.intro_tasks_title), + message = stringResource(R.string.intro_tasks_text1), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp) + ) { + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_jtx), + summary = { + Text(stringResource(R.string.intro_tasks_jtx_info)) + }, + isSelected = jtxSelected, + isToggled = jtxRequested, + enabled = jtxInstalled, + onSelected = { onProviderSelected(ProviderName.JtxBoard) }, + onToggled = { toggled -> + if (toggled) installApp(ProviderName.JtxBoard.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) - override fun onCleared() { - settings.removeOnChangeListener(this) - tasksWatcher.close() - } + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_tasks_org), + summary = { + val summary = HtmlCompat.fromHtml( + stringResource(R.string.intro_tasks_tasks_org_info), + HtmlCompat.FROM_HTML_MODE_COMPACT + ).toAnnotatedString() - @AnyThread - fun checkInstalled() { - val taskProvider = TaskUtils.currentProvider(context) - currentProvider.postValue(taskProvider) + ClickableText( + text = summary, + onClick = { index -> + // Get the tapped position, and check if there's any link + summary.getUrlAnnotations(index, index).firstOrNull()?.item?.url?.let { url -> + UiUtils.launchUri(context, Uri.parse(url)) + } + } + ) + }, + isSelected = tasksOrgSelected, + isToggled = tasksOrgRequested, + enabled = tasksOrgInstalled, + onSelected = { onProviderSelected(ProviderName.TasksOrg) }, + onToggled = { toggled -> + if (toggled) installApp(ProviderName.TasksOrg.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) - val openTasks = isInstalled(ProviderName.OpenTasks.packageName) - openTasksInstalled.postValue(openTasks) - openTasksRequested.postValue(openTasks) - openTasksSelected.postValue(taskProvider == ProviderName.OpenTasks) + RadioWithSwitch( + title = stringResource(R.string.intro_tasks_opentasks), + summary = { + Text(stringResource(R.string.intro_tasks_opentasks_info)) + }, + isSelected = openTasksSelected, + isToggled = openTasksRequested, + enabled = openTasksInstalled, + onSelected = { onProviderSelected(ProviderName.OpenTasks) }, + onToggled = { toggled -> + if (toggled) installApp(ProviderName.OpenTasks.packageName) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) - val tasksOrg = isInstalled(ProviderName.TasksOrg.packageName) - tasksOrgInstalled.postValue(tasksOrg) - tasksOrgRequested.postValue(tasksOrg) - tasksOrgSelected.postValue(taskProvider == ProviderName.TasksOrg) - - val jtxBoard = isInstalled(ProviderName.JtxBoard.packageName) - jtxInstalled.postValue(jtxBoard) - jtxRequested.postValue(jtxBoard) - jtxSelected.postValue(taskProvider == ProviderName.JtxBoard) - } - - private fun isInstalled(packageName: String): Boolean = - try { - context.packageManager.getPackageInfo(packageName, 0) - true - } catch (e: PackageManager.NameNotFoundException) { - false + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + Checkbox( + checked = dontShow, + onCheckedChange = { model.dontShow.value = it } + ) + Text( + text = stringResource(R.string.intro_tasks_dont_show), + modifier = Modifier + .fillMaxWidth() + .clickable { model.dontShow.value = !dontShow } + ) } + } - fun selectPreferredProvider(provider: ProviderName) { - TaskUtils.setPreferredProvider(context, provider) + Text( + text = stringResource( + R.string.intro_leave_unchecked, + stringResource(R.string.app_settings_reset_hints) + ), + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) } - - - override fun onSettingsChanged() { - checkInstalled() - } - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt index 9a2916a9..65e11194 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroFragment.kt @@ -9,19 +9,32 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment -import at.bitfire.davdroid.App -import at.bitfire.davdroid.BuildConfig -import at.bitfire.davdroid.R +import androidx.fragment.app.viewModels import at.bitfire.davdroid.resource.TaskUtils import at.bitfire.davdroid.settings.SettingsManager -import at.bitfire.davdroid.ui.TasksFragment +import at.bitfire.davdroid.ui.TasksCard +import at.bitfire.davdroid.ui.TasksModel +import com.google.accompanist.themeadapter.material.MdcTheme +import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +@AndroidEntryPoint class TasksIntroFragment : Fragment() { + val model: TasksModel by viewModels() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.intro_tasks, container, false) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MdcTheme { + TasksCard(model) + } + } + } + } class Factory @Inject constructor( @@ -29,7 +42,7 @@ class TasksIntroFragment : Fragment() { ): IntroFragmentFactory { override fun getOrder(context: Context): Int { - return if (!TaskUtils.isAvailable(context) && settingsManager.getBooleanOrNull(TasksFragment.Model.HINT_OPENTASKS_NOT_INSTALLED) != false) + return if (!TaskUtils.isAvailable(context) && settingsManager.getBooleanOrNull(TasksModel.HINT_OPENTASKS_NOT_INSTALLED) != false) 10 else IntroFragmentFactory.DONT_SHOW diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CardWithImage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CardWithImage.kt new file mode 100644 index 00000000..e71b8a9e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/CardWithImage.kt @@ -0,0 +1,76 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.ui.widget + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.bitfire.davdroid.R + +@Composable +fun CardWithImage( + image: Painter, + title: String, + message: String, + modifier: Modifier = Modifier, + imageContentDescription: String? = null, + content: @Composable ColumnScope.() -> Unit = {} +) { + Card(modifier) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = image, + contentDescription = imageContentDescription, + modifier = Modifier.fillMaxWidth() + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + text = title, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + style = MaterialTheme.typography.h6 + ) + Text( + text = message, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + style = MaterialTheme.typography.body1 + ) + + content() + } + } + } +} + +@Preview +@Composable +fun CardWithImagePreview() { + CardWithImage( + image = painterResource(R.drawable.intro_tasks), + title = "Demo card", + message = "This is the message to be displayed under the title, but before the content." + ) +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/RadioWithSwitch.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/RadioWithSwitch.kt new file mode 100644 index 00000000..3ac94750 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/widget/RadioWithSwitch.kt @@ -0,0 +1,101 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.ui.widget + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.RadioButton +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +/** + * Provides a radio button with a text, a switch at the end, and an optional summary to be shown + * under the main text. + * + * @param title The "proper" text of the Radio button. Shown in the middle of the row, between the + * radio button and the switch. + * @param summary If not `null`, shown below the title. Used to give more context or information. + * Supports formatting and interactions. + * @param isSelected Whether the item is currently selected. Refers to the radio button. + * @param isToggled Whether the switch is toggled. + * @param modifier Any modifiers to apply to the row. + * @param enabled Whether the radio button should be enabled. The enabled state of the switch is + * reverse from this. So if it's `true`, the switch will be disabled. + * @param onSelected Gets called whenever the user requests this row to be enabled. Either by + * selecting the radio button or tapping the text. + * @param onToggled Gets called whenever the switch gets updated. Contains the checked status. + */ +@Composable +fun RadioWithSwitch( + title: String, + summary: (@Composable () -> Unit)?, + isSelected: Boolean, + isToggled: Boolean, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onSelected: () -> Unit, + onToggled: (Boolean) -> Unit +) { + Row(modifier) { + RadioButton(selected = isSelected, onClick = onSelected, enabled = enabled) + + Column( + modifier = Modifier + .weight(1f) + .clickable(enabled = enabled, role = Role.RadioButton, onClick = onSelected) + ) { + Text( + text = title, + color = LocalContentColor.current.copy( + alpha = if (enabled) 1f else ContentAlpha.disabled + ), + style = MaterialTheme.typography.body1, + modifier = Modifier.fillMaxWidth() + ) + summary?.let { sum -> + ProvideTextStyle( + value = MaterialTheme.typography.body2.copy( + color = LocalContentColor.current.copy( + alpha = if (enabled) 1f else ContentAlpha.disabled + ) + ) + ) { + sum() + } + } + } + + Switch( + checked = isToggled, + onCheckedChange = onToggled, + enabled = !enabled + ) + } +} + +@Preview +@Composable +private fun RadioWithSwitch_Preview() { + RadioWithSwitch( + title = "RadioWithSwitch Preview", + summary = { Text("An example summary") }, + isSelected = true, + isToggled = false, + onSelected = { }, + onToggled = { } + ) +} diff --git a/app/src/main/res/layout/activity_tasks.xml b/app/src/main/res/layout/activity_tasks.xml deleted file mode 100644 index dc46bb1c..00000000 --- a/app/src/main/res/layout/activity_tasks.xml +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/intro_tasks.xml b/app/src/main/res/layout/intro_tasks.xml deleted file mode 100644 index d312d863..00000000 --- a/app/src/main/res/layout/intro_tasks.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file