mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-15 15:59:18 +00:00
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:
parent
da5b765b3a
commit
c087834452
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,99 +9,55 @@ 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() {
|
||||
|
||||
private var _binding: ActivityTasksBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
val model by viewModels<Model>()
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
else
|
||||
Snackbar.make(binding.frame, R.string.intro_tasks_no_app_store, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
class TasksModel @Inject constructor(
|
||||
application: Application,
|
||||
val settings: SettingsManager
|
||||
) : AndroidViewModel(application), SettingsManager.OnChangeListener {
|
||||
|
@ -117,8 +73,6 @@ class TasksFragment: Fragment() {
|
|||
|
||||
}
|
||||
|
||||
val context: Context get() = getApplication()
|
||||
|
||||
val currentProvider = MutableLiveData<ProviderName>()
|
||||
val openTasksInstalled = MutableLiveData<Boolean>()
|
||||
val openTasksRequested = MutableLiveData<Boolean>()
|
||||
|
@ -129,36 +83,39 @@ class TasksFragment: Fragment() {
|
|||
val jtxInstalled = MutableLiveData<Boolean>()
|
||||
val jtxRequested = MutableLiveData<Boolean>()
|
||||
val jtxSelected = MutableLiveData<Boolean>()
|
||||
val tasksWatcher = object: PackageChangedReceiver(context) {
|
||||
|
||||
private val tasksWatcher = object: PackageChangedReceiver(application) {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
checkInstalled()
|
||||
}
|
||||
}
|
||||
|
||||
val dontShow = object: ObservableBoolean() {
|
||||
override fun get() = settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false
|
||||
override fun set(dontShowAgain: Boolean) {
|
||||
if (dontShowAgain)
|
||||
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
|
||||
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
|
||||
notifyChange()
|
||||
}
|
||||
}
|
||||
|
||||
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(context)
|
||||
val taskProvider = TaskUtils.currentProvider(getApplication())
|
||||
currentProvider.postValue(taskProvider)
|
||||
|
||||
val openTasks = isInstalled(ProviderName.OpenTasks.packageName)
|
||||
|
@ -179,14 +136,15 @@ class TasksFragment: Fragment() {
|
|||
|
||||
private fun isInstalled(packageName: String): Boolean =
|
||||
try {
|
||||
context.packageManager.getPackageInfo(packageName, 0)
|
||||
getApplication<Application>().packageManager.getPackageInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun selectPreferredProvider(provider: ProviderName) {
|
||||
TaskUtils.setPreferredProvider(context, provider)
|
||||
// Changes preferred task app setting, so onSettingsChanged() will be called
|
||||
TaskUtils.setPreferredProvider(getApplication(), provider)
|
||||
}
|
||||
|
||||
|
||||
|
@ -196,4 +154,160 @@ class TasksFragment: Fragment() {
|
|||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun TasksCard(
|
||||
model: TasksModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
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 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onProviderSelected(provider: ProviderName) {
|
||||
if (model.currentProvider.value != provider)
|
||||
model.selectPreferredProvider(provider)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
}
|
|
@ -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 = { }
|
||||
)
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in a new issue