Rewrite OpenSourceFragment to Compose (#581)

* Migrated to Jetpack Compose

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

* Removed layout file

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

* Adapt paddings and font size

* Added check for non-available browsers

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

* Add SafeAndroidUriHandler

---------

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 2024-02-19 19:09:49 +01:00 committed by GitHub
parent af00ff0c4c
commit f6ac4e02d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 137 additions and 126 deletions

View file

@ -85,6 +85,7 @@ object UiUtils {
* @return true on success, false if the Intent could not be resolved (for instance, because
* there is no user agent installed)
*/
@Deprecated("Use SafeAndroidUriHandler (Compose) instead")
fun launchUri(context: Context, uri: Uri, action: String = Intent.ACTION_VIEW, toastInstallBrowser: Boolean = true): Boolean {
val intent = Intent(action, uri)
try {

View file

@ -9,16 +9,47 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.databinding.ObservableBoolean
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import at.bitfire.davdroid.App
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.IntroOpenSourceBinding
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.UiUtils
import at.bitfire.davdroid.ui.intro.OpenSourceFragment.Model.Companion.SETTING_NEXT_DONATION_POPUP
import at.bitfire.davdroid.ui.widget.CardWithImage
import at.bitfire.davdroid.ui.widget.SafeAndroidUriHandler
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@ -30,18 +61,83 @@ class OpenSourceFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = IntroOpenSourceBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
binding.model = model
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MdcTheme {
var dontShow by remember { mutableStateOf(model.dontShow.get()) }
binding.text.text = getString(R.string.intro_open_source_text, getString(R.string.app_name))
binding.moreInfo.setOnClickListener {
UiUtils.launchUri(requireActivity(), App.homepageUrl(requireActivity()).buildUpon()
.appendPath("donate")
.build())
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
FragmentContent(
dontShow = dontShow,
onChangeDontShow = {
model.dontShow.set(it)
dontShow = it
}
)
}
}
}
}
}
return binding.root
@Preview(
showBackground = true,
showSystemUi = true
)
@Composable
fun FragmentContent(
dontShow: Boolean = false,
onChangeDontShow: (Boolean) -> Unit = {}
) {
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp)
) {
CardWithImage(
title = stringResource(R.string.intro_open_source_title),
image = painterResource(R.drawable.intro_open_source),
imageContentScale = ContentScale.Inside,
message = stringResource(
R.string.intro_open_source_text,
stringResource(R.string.app_name)
)
) {
OutlinedButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(requireActivity())
.buildUpon()
.appendPath("donate")
.build()
.toString()
)
}
) {
Text(stringResource(R.string.intro_open_source_details))
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = dontShow,
onCheckedChange = onChangeDontShow
)
Text(
text = stringResource(R.string.intro_battery_dont_show),
style = MaterialTheme.typography.body2,
modifier = Modifier.clickable { onChangeDontShow(!dontShow) }
)
}
}
Spacer(Modifier.height(90.dp))
}
}

View file

@ -24,7 +24,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
@ -38,14 +37,13 @@ fun CardWithImage(
image: Painter? = null,
imageContentDescription: String? = null,
imageAlignment: Alignment = Alignment.Center,
imageContentScale: ContentScale = ContentScale.Crop,
message: String? = null,
subtitle: String? = null,
icon: ImageVector? = null,
iconContentDescription: String? = null,
content: @Composable ColumnScope.() -> Unit = {}
) {
val configuration = LocalConfiguration.current
Card(modifier) {
Column(
modifier = Modifier.fillMaxWidth()
@ -57,7 +55,7 @@ fun CardWithImage(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = dimensionResource(R.dimen.card_theme_max_height)),
contentScale = ContentScale.Crop,
contentScale = imageContentScale,
alignment = imageAlignment
)
}
@ -70,7 +68,7 @@ fun CardWithImage(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
.padding(top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
icon?.let {
@ -103,7 +101,7 @@ fun CardWithImage(
text = it,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
.padding(vertical = 4.dp),
style = MaterialTheme.typography.body1
)
}

View file

@ -0,0 +1,25 @@
package at.bitfire.davdroid.ui.widget
import android.content.Context
import android.widget.Toast
import androidx.compose.ui.platform.AndroidUriHandler
import androidx.compose.ui.platform.UriHandler
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import java.util.logging.Level
class SafeAndroidUriHandler(
val context: Context
): UriHandler {
override fun openUri(uri: String) {
try {
AndroidUriHandler(context).openUri(uri)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "No browser available", e)
// no browser available
Toast.makeText(context, R.string.install_browser, Toast.LENGTH_LONG).show()
}
}
}

View file

@ -1,109 +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">
<data>
<variable name="model" type="at.bitfire.davdroid.ui.intro.OpenSourceFragment.Model"/>
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/appintro2_bottombar_height"
android:background="?android:attr/colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
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">
<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_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/heading"
android:adjustViewBounds="true"
app:verticalOffsetPercent=".45"
app:srcCompat="@drawable/intro_open_source"/>
<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_open_source_title"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@id/text"
app:layout_constraintEnd_toStartOf="@id/end"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/image" />
<TextView
android:id="@+id/text"
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_open_source_text"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@id/moreInfo"
app:layout_constraintEnd_toStartOf="@id/end"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/heading" />
<Button
android:id="@+id/moreInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/text"
app:layout_constraintBottom_toTopOf="@id/dontShow"
app:layout_constraintStart_toEndOf="@id/start"
android:layout_marginTop="16dp"
android:layout_gravity="center"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:text="@string/intro_open_source_details" />
<CheckBox
android:id="@+id/dontShow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="@={model.dontShow}"
android:text="@string/intro_open_source_dont_show"
android:textAlignment="viewStart"
app:layout_constraintEnd_toStartOf="@id/end"
app:layout_constraintStart_toEndOf="@id/start"
app:layout_constraintTop_toBottomOf="@id/moreInfo" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
</layout>