Rewrite navigation drawer to Compose (#644)

(sync bitfireAT/davx5#545)
This commit is contained in:
Ricki Hirner 2024-03-12 12:55:38 +01:00 committed by GitHub
parent 75a0c77b5f
commit 33e726a7b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 395 additions and 180 deletions

View file

@ -25,6 +25,8 @@ object Constants {
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
val FEDIVERSE_HANDLE = "@davx5app@fosstodon.org"
val FEDIVERSE_URL = "https://fosstodon.org/@davx5app".toUri()
/**

View file

@ -73,7 +73,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
@ -94,8 +93,6 @@ import at.bitfire.davdroid.ui.widget.ActionCard
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.material.navigation.NavigationView
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
@ -133,8 +130,10 @@ class AccountsActivity: AppCompatActivity() {
setContent {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scaffoldState = rememberScaffoldState(
snackbarHostState = snackbarHostState
)
val refreshing by remember { mutableStateOf(false) }
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = {
@ -146,10 +145,18 @@ class AccountsActivity: AppCompatActivity() {
AppTheme {
Scaffold(
scaffoldState = scaffoldState,
drawerContent = drawerContent(scope, scaffoldState),
drawerContent = {
accountsDrawerHandler.AccountsDrawer(
snackbarHostState = snackbarHostState,
onCloseDrawer = {
scope.launch {
scaffoldState.drawerState.close()
}
}
)
},
topBar = topBar(scope, scaffoldState, accounts?.isNotEmpty() == true),
floatingActionButton = floatingActionButton(),
snackbarHost = snackbarHost(snackbarHostState, scope)
floatingActionButton = floatingActionButton()
) { padding ->
Box(
Modifier
@ -329,6 +336,9 @@ class AccountsActivity: AppCompatActivity() {
scaffoldState: ScaffoldState
): @Composable (ColumnScope.() -> Unit) =
{
/*
LEGACY
AndroidView(factory = { context ->
// use legacy NavigationView for now
NavigationView(context).apply {
@ -349,6 +359,7 @@ class AccountsActivity: AppCompatActivity() {
}
}
}, modifier = Modifier.fillMaxWidth())
*/
}

View file

@ -4,15 +4,278 @@
package at.bitfire.davdroid.ui
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.view.Menu
import android.view.MenuItem
import android.content.Intent
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.SnackbarResult
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Feedback
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import kotlinx.coroutines.launch
import java.net.URI
interface AccountsDrawerHandler {
abstract class AccountsDrawerHandler {
fun initMenu(context: Context, menu: Menu)
open class CloseDrawerHandler {
open fun closeDrawer() {}
}
fun onNavigationItemSelected(activity: Activity, item: MenuItem)
val localCloseDrawerHandler = compositionLocalOf {
CloseDrawerHandler()
}
@Composable
abstract fun MenuEntries(
snackbarHostState: SnackbarHostState
)
@Composable
fun AccountsDrawer(
snackbarHostState: SnackbarHostState,
onCloseDrawer: () -> Unit
) {
Column(Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Header()
val closeDrawerHandler = object : CloseDrawerHandler() {
override fun closeDrawer() {
onCloseDrawer()
}
}
CompositionLocalProvider(localCloseDrawerHandler provides closeDrawerHandler) {
MenuEntries(snackbarHostState)
}
}
}
// menu section composables
@Composable
open fun ImportantEntries(
snackbarHostState: SnackbarHostState
) {
val context = LocalContext.current
val isBeta =
LocalInspectionMode.current ||
BuildConfig.VERSION_NAME.contains("-alpha") ||
BuildConfig.VERSION_NAME.contains("-beta") ||
BuildConfig.VERSION_NAME.contains("-rc")
val scope = rememberCoroutineScope()
MenuEntry(
icon = Icons.Default.Info,
title = stringResource(R.string.navigation_drawer_about),
onClick = {
context.startActivity(Intent(context, AboutActivity::class.java))
}
)
if (isBeta)
MenuEntry(
icon = Icons.Default.Feedback,
title = stringResource(R.string.navigation_drawer_beta_feedback),
onClick = {
onBetaFeedback(
context,
onShowSnackbar = { text: String, actionLabel: String, action: () -> Unit ->
scope.launch {
if (snackbarHostState.showSnackbar(text, actionLabel) == SnackbarResult.ActionPerformed)
action()
}
}
)
}
)
MenuEntry(
icon = Icons.Default.Settings,
title = stringResource(R.string.navigation_drawer_settings),
onClick = {
context.startActivity(Intent(context, AppSettingsActivity::class.java))
}
)
}
@Composable
fun Tools() {
val context = LocalContext.current
MenuHeading(R.string.navigation_drawer_tools)
MenuEntry(
icon = Icons.Default.Storage,
title = stringResource(R.string.webdav_mounts_title),
onClick = {
context.startActivity(Intent(context, WebdavMountsActivity::class.java))
}
)
}
// overridable actions
open fun onBetaFeedback(
context: Context,
onShowSnackbar: (message: String, actionLabel: String, action: () -> Unit) -> Unit
) {
val mailto = URI(
"mailto", "play@bitfire.at?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})", null
)
val intent = Intent(Intent.ACTION_SENDTO, mailto.toString().toUri())
try {
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
}
}
// building blocks
@Composable
fun Header() {
Column(
Modifier
.background(Color.DarkGray)
.fillMaxWidth()
.padding(16.dp)
) {
Spacer(Modifier.height(16.dp))
Box(
Modifier
.background(
color = MaterialTheme.colors.primary,
shape = RoundedCornerShape(16.dp)
)
) {
Icon(
painterResource(R.drawable.ic_launcher_foreground),
stringResource(R.string.app_name),
tint = Color.White,
modifier = Modifier
.scale(1.2f)
.height(56.dp)
.width(56.dp)
)
}
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.app_name),
color = Color.White,
style = MaterialTheme.typography.body1
)
Text(
stringResource(R.string.navigation_drawer_subtitle),
color = Color.White.copy(alpha = 0.7f),
style = MaterialTheme.typography.body2
)
}
Spacer(Modifier.height(8.dp))
}
@Composable
fun MenuHeading(text: String) {
Divider(Modifier.padding(vertical = 8.dp))
Text(
text,
style = MaterialTheme.typography.body2,
modifier = Modifier.padding(8.dp)
)
}
@Composable
fun MenuHeading(@StringRes text: Int) = MenuHeading(stringResource(text))
@Composable
fun MenuEntry(
icon: Painter,
title: String,
onClick: () -> Unit
) {
val closeHandler = localCloseDrawerHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(onClick = {
onClick()
closeHandler.closeDrawer()
})
.padding(4.dp)
) {
Icon(
icon,
contentDescription = title,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
Text(
title,
style = MaterialTheme.typography.body2,
modifier = Modifier
.padding(start = 8.dp)
.weight(1f)
)
}
}
@Composable
fun MenuEntry(
icon: ImageVector,
title: String,
onClick: () -> Unit
) {
MenuEntry(
icon = rememberVectorPainter(icon),
title = title,
onClick = onClick
)
}
}

View file

@ -1,48 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.CallSuper
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import com.google.android.material.snackbar.Snackbar
/**
* Default menu items control
*/
abstract class BaseAccountsDrawerHandler: AccountsDrawerHandler {
companion object {
private const val BETA_FEEDBACK_URI = "mailto:play@bitfire.at?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
}
@CallSuper
override fun initMenu(context: Context, menu: Menu) {
if (BuildConfig.VERSION_NAME.contains("-alpha") || BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
menu.findItem(R.id.nav_beta_feedback).isVisible = true
}
@CallSuper
override fun onNavigationItemSelected(activity: Activity, item: MenuItem) {
when (item.itemId) {
R.id.nav_about ->
activity.startActivity(Intent(activity, AboutActivity::class.java))
R.id.nav_beta_feedback -> {
if (!UiUtils.launchUri(activity, Uri.parse(BETA_FEEDBACK_URI), Intent.ACTION_SENDTO, false))
Snackbar.make(activity.window.findViewById(android.R.id.content), R.string.install_email_client, Snackbar.LENGTH_LONG).show()
}
R.id.nav_app_settings ->
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
}
}
}

View file

@ -4,69 +4,124 @@
package at.bitfire.davdroid.ui
import android.app.Activity
import android.content.Intent
import android.view.MenuItem
import androidx.compose.foundation.layout.Column
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.Forum
import androidx.compose.material.icons.filled.HelpCenter
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.VolunteerActivism
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.COMMUNITY_URL
import at.bitfire.davdroid.Constants.FEDIVERSE_URL
import at.bitfire.davdroid.Constants.MANUAL_URL
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import javax.inject.Inject
/**
* Default menu items control
*/
class OseAccountsDrawerHandler @Inject constructor(): BaseAccountsDrawerHandler() {
open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler() {
override fun onNavigationItemSelected(activity: Activity, item: MenuItem) {
val homepageUrl = Constants.HOMEPAGE_URL.buildUpon()
.withStatParams("OseAccountsDrawerHandler")
@Composable
override fun MenuEntries(
snackbarHostState: SnackbarHostState
) {
val uriHandler = LocalUriHandler.current
when (item.itemId) {
// Most important entries
ImportantEntries(snackbarHostState)
R.id.nav_mastodon ->
UiUtils.launchUri(
activity,
FEDIVERSE_URL
)
// News
MenuHeading(R.string.navigation_drawer_news_updates)
MenuEntry(
icon = painterResource(R.drawable.mastodon),
title = Constants.FEDIVERSE_HANDLE,
onClick = {
uriHandler.openUri(FEDIVERSE_URL.toString())
}
)
R.id.nav_webdav_mounts ->
activity.startActivity(Intent(activity, WebdavMountsActivity::class.java))
// Tools
Tools()
R.id.nav_website ->
UiUtils.launchUri(
activity,
homepageUrl.build()
)
R.id.nav_manual ->
UiUtils.launchUri(
activity,
MANUAL_URL
)
R.id.nav_faq ->
UiUtils.launchUri(
activity,
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_FAQ).build()
)
R.id.nav_community ->
UiUtils.launchUri(activity, COMMUNITY_URL)
R.id.nav_donate ->
UiUtils.launchUri(
activity,
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_OPEN_SOURCE).build()
)
R.id.nav_privacy ->
UiUtils.launchUri(
activity,
homepageUrl.appendPath(Constants.HOMEPAGE_PATH_PRIVACY).build()
)
// Support the project
SupportUs(onContribute = {
uriHandler.openUri(
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_OPEN_SOURCE)
.build().toString()
)
})
else ->
super.onNavigationItemSelected(activity, item)
// External links
MenuHeading(R.string.navigation_drawer_external_links)
MenuEntry(
icon = Icons.Default.Home,
title = stringResource(R.string.navigation_drawer_website),
onClick = {
uriHandler.openUri(Constants.HOMEPAGE_URL.toString())
}
)
MenuEntry(
icon = Icons.Default.Info,
title = stringResource(R.string.navigation_drawer_manual),
onClick = {
uriHandler.openUri(MANUAL_URL.toString())
}
)
MenuEntry(
icon = Icons.Default.HelpCenter,
title = stringResource(R.string.navigation_drawer_faq),
onClick = {
uriHandler.openUri(
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_FAQ)
.build().toString()
)
}
)
MenuEntry(
icon = Icons.Default.Forum,
title = stringResource(R.string.navigation_drawer_community),
onClick = {
uriHandler.openUri(COMMUNITY_URL.toString())
}
)
MenuEntry(
icon = Icons.Default.CloudOff,
title = stringResource(R.string.navigation_drawer_privacy_policy),
onClick = {
uriHandler.openUri(
Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_PRIVACY)
.build().toString()
)
}
)
}
@Composable
@Preview
fun MenuEntries_Standard_Preview() {
Column {
MenuEntries(SnackbarHostState())
}
}
@Composable
open fun SupportUs(onContribute: () -> Unit) {
MenuHeading(R.string.navigation_drawer_support_project)
MenuEntry(
icon = Icons.Default.VolunteerActivism,
title = stringResource(R.string.navigation_drawer_contribute),
onClick = onContribute
)
}
}

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_about"
android:icon="@drawable/ic_info"
android:title="@string/navigation_drawer_about"/>
<item
android:id="@+id/nav_beta_feedback"
android:icon="@drawable/ic_forum"
android:title="@string/navigation_drawer_beta_feedback"
android:visible="false"/>
<item
android:id="@+id/nav_app_settings"
android:icon="@drawable/ic_settings"
android:title="@string/navigation_drawer_settings"/>
<item android:title="@string/navigation_drawer_news_updates">
<menu>
<item
android:id="@+id/nav_mastodon"
android:icon="@drawable/mastodon"
android:title="\@davx5app\@fosstodon.org"
tools:ignore="HardcodedText"/>
</menu>
</item>
<item android:title="@string/navigation_drawer_tools">
<menu>
<item
android:id="@+id/nav_webdav_mounts"
android:icon="@drawable/ic_storage"
android:title="@string/webdav_mounts_title" />
</menu>
</item>
<item android:title="@string/navigation_drawer_external_links">
<menu>
<item
android:id="@+id/nav_website"
android:icon="@drawable/ic_home"
android:title="@string/navigation_drawer_website"/>
<item
android:id="@+id/nav_manual"
android:icon="@drawable/ic_info"
android:title="@string/navigation_drawer_manual"/>
<item
android:id="@+id/nav_faq"
android:icon="@drawable/ic_help"
android:title="@string/navigation_drawer_faq"/>
<item
android:id="@+id/nav_community"
android:icon="@drawable/ic_forum"
android:title="@string/navigation_drawer_community"/>
<item
android:id="@+id/nav_donate"
android:icon="@drawable/ic_attach_money"
android:title="@string/navigation_drawer_donate"/>
<item
android:id="@+id/nav_privacy"
android:icon="@drawable/ic_cloud_off"
android:title="@string/navigation_drawer_privacy_policy"/>
</menu>
</item>
</menu>

View file

@ -136,7 +136,8 @@
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_community">Community</string>
<string name="navigation_drawer_donate">Donate</string>
<string name="navigation_drawer_support_project">Support the project</string>
<string name="navigation_drawer_contribute">How to contribute</string>
<string name="navigation_drawer_privacy_policy">Privacy policy</string>
<string name="account_list_no_notification_permission">Notifications disabled. You won\'t be notified about sync errors.</string>
<string name="account_list_no_internet">No validated Internet connectivity. Synchronization may not run.</string>