Rewrite TasksFragment to Compose (#481)

* Added `CardWithImage`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added `RadioWithSwitch`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Migrating to Compose

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added observers

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Fixed functions signature

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added kdoc

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Removed layout

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Color for disabled

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added "don't show" behaviour

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added all tasks providers

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Moved checkbox to correct location

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Fixed don't need behaviour

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added theme

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added todo

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added support for link annotations

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added support for annotated strings and urls

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added tests for HTML annotation

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Extracted `linkStyle`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Removed observers for requested

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Removed more observers

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added multiple links test

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Moved `installApp` to `TasksCard`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Moved all model calls to composable

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Removed preview since not usable

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Got rid of TasksFragment

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Fixed import

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Switched link color to orange

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added missing copyright information

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Use HtmlCompat and existing Spanned.toAnnotatedString

* Added default content

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Renamed image content description

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Got rid of empty content

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Made summary of RadioWithSwitch composable

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Added missing entry point annotation

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Added click handling for tasks org

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Got rid of the preview provider

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Minor changes

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Arnau Mora 2023-12-15 13:09:42 +01:00 committed by GitHub
parent da5b765b3a
commit c087834452
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 470 additions and 389 deletions

View file

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

View file

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

View file

@ -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<Model>()
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<ProviderName>()
val openTasksInstalled = MutableLiveData<Boolean>()
val openTasksRequested = MutableLiveData<Boolean>()
val openTasksSelected = MutableLiveData<Boolean>()
val tasksOrgInstalled = MutableLiveData<Boolean>()
val tasksOrgRequested = MutableLiveData<Boolean>()
val tasksOrgSelected = MutableLiveData<Boolean>()
val jtxInstalled = MutableLiveData<Boolean>()
val jtxRequested = MutableLiveData<Boolean>()
val jtxSelected = MutableLiveData<Boolean>()
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<Boolean> { 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<Application>().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<ProviderName>()
val openTasksInstalled = MutableLiveData<Boolean>()
val openTasksRequested = MutableLiveData<Boolean>()
val openTasksSelected = MutableLiveData<Boolean>()
val tasksOrgInstalled = MutableLiveData<Boolean>()
val tasksOrgRequested = MutableLiveData<Boolean>()
val tasksOrgSelected = MutableLiveData<Boolean>()
val jtxInstalled = MutableLiveData<Boolean>()
val jtxRequested = MutableLiveData<Boolean>()
val jtxSelected = MutableLiveData<Boolean>()
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()
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,213 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.TasksActivity">
<data>
<import type="android.view.View"/>
<variable name="model" type="at.bitfire.davdroid.ui.TasksFragment.Model"/>
</data>
<ScrollView
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/card_padding">
<at.bitfire.davdroid.ui.widget.CropImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="@dimen/card_theme_max_height"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/heading"
android:adjustViewBounds="true"
app:srcCompat="@drawable/intro_tasks"
app:verticalOffsetPercent=".55" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/card_padding" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="@dimen/card_padding" />
<TextView
android:id="@+id/heading"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:text="@string/intro_tasks_title"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@id/text1"
app:layout_constraintEnd_toStartOf="@id/end"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/image" />
<TextView
android:id="@+id/text1"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:text="@string/intro_tasks_text1"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@id/openTasksRadio"
app:layout_constraintEnd_toEndOf="@id/end"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/heading" />
<RadioButton
android:id="@+id/jtxRadio"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:checked="@={model.jtxSelected}"
android:clickable="@{model.jtxInstalled}"
android:text="@string/intro_tasks_jtx"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/jtxSwitch"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/text1" />
<TextView
android:id="@+id/jtxInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/jtxRadio"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/end"
style="@style/TextAppearance.MaterialComponents.Body2"
app:html="@{@string/intro_tasks_jtx_info}" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/jtxSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={model.jtxRequested}"
android:clickable="@{!model.jtxInstalled}"
app:layout_constraintTop_toTopOf="@id/jtxRadio"
app:layout_constraintBottom_toBottomOf="@id/jtxRadio"
app:layout_constraintStart_toEndOf="@id/jtxRadio"
app:layout_constraintEnd_toEndOf="@id/end"/>
<RadioButton
android:id="@+id/tasksOrgRadio"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:checked="@={model.tasksOrgSelected}"
android:clickable="@{model.tasksOrgInstalled}"
android:text="@string/intro_tasks_tasks_org"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/tasksOrgSwitch"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/jtxInfo" />
<TextView
android:id="@+id/tasksOrgInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tasksOrgRadio"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/end"
style="@style/TextAppearance.MaterialComponents.Body2"
app:html="@{@string/intro_tasks_tasks_org_info}" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/tasksOrgSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={model.tasksOrgRequested}"
android:clickable="@{!model.tasksOrgInstalled}"
app:layout_constraintTop_toTopOf="@id/tasksOrgRadio"
app:layout_constraintBottom_toBottomOf="@id/tasksOrgRadio"
app:layout_constraintStart_toEndOf="@id/tasksOrgRadio"
app:layout_constraintEnd_toEndOf="@id/end"/>
<RadioButton
android:id="@+id/openTasksRadio"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:checked="@={model.openTasksSelected}"
android:clickable="@{model.openTasksInstalled}"
android:text="@string/intro_tasks_opentasks"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/openTasksSwitch"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/tasksOrgInfo" />
<TextView
android:id="@+id/openTasksInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/openTasksRadio"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintEnd_toStartOf="@id/end"
style="@style/TextAppearance.MaterialComponents.Body2"
app:html="@{@string/intro_tasks_opentasks_info}"/>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/openTasksSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={model.openTasksRequested}"
android:clickable="@{!model.openTasksInstalled}"
app:layout_constraintTop_toTopOf="@id/openTasksRadio"
app:layout_constraintBottom_toBottomOf="@id/openTasksRadio"
app:layout_constraintStart_toEndOf="@id/openTasksRadio"
app:layout_constraintEnd_toEndOf="@id/end"/>
<CheckBox
android:id="@+id/dontShow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
android:checked="@={model.dontShow}"
android:text="@string/intro_tasks_dont_show"
android:textAlignment="viewStart"
android:visibility="@{model.openTasksInstalled ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/end"
app:layout_constraintStart_toStartOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/openTasksInfo" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/infoLeaveUnchecked"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/intro_leave_unchecked"
android:textAlignment="viewStart" />
</LinearLayout>
</ScrollView>
</layout>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/appintro2_bottombar_height">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="at.bitfire.davdroid.ui.TasksFragment"/>
</FrameLayout>