Rewrite About activity to Compose (bitfireAT/davx5#432)

This commit is contained in:
Ricki Hirner 2023-11-07 14:12:49 +01:00
parent cf9340107f
commit 4a200dfbb7
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
12 changed files with 410 additions and 442 deletions

View file

@ -131,6 +131,7 @@ dependencies {
kapt "com.google.dagger:hilt-android-compiler:${versions.hilt}" // replace by KSP when ready [https://issuetracker.google.com/179057202]
// support libs
implementation 'androidx.activity:activity-compose:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.6.0'
implementation 'androidx.cardview:cardview:1.0.0'
@ -141,6 +142,7 @@ dependencies {
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'androidx.preference:preference-ktx:1.2.1'

View file

@ -5,38 +5,54 @@
package at.bitfire.davdroid.ui
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.util.DisplayMetrics
import android.view.*
import androidx.annotation.UiThread
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Home
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.core.text.HtmlCompat
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.fragment.app.viewModels
import androidx.compose.ui.graphics.Color
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.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AboutBinding
import at.bitfire.davdroid.databinding.AboutLanguagesBinding
import at.bitfire.davdroid.databinding.AboutTranslationBinding
import at.bitfire.davdroid.databinding.ActivityAboutBinding
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.widget.PixelBoxes
import com.google.accompanist.themeadapter.material.MdcTheme
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import dagger.BindsOptionalOf
@ -47,282 +63,269 @@ import dagger.hilt.android.components.ActivityComponent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import org.json.JSONException
import org.json.JSONObject
import java.text.Collator
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import java.util.logging.Level
import javax.inject.Inject
import javax.inject.Qualifier
import kotlin.jvm.optionals.getOrNull
@AndroidEntryPoint
class AboutActivity: AppCompatActivity() {
companion object {
val model by viewModels<Model>()
const val pixelsHtml = "<font color=\"#fff433\">■</font>" +
"<font color=\"#ffffff\">■</font>" +
"<font color=\"#9b59d0\">■</font>" +
"<font color=\"#000000\">■</font>"
}
private lateinit var binding: ActivityAboutBinding
@Inject
lateinit var licenseInfoProvider: Optional<AppLicenseInfoProvider>
@OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.viewpager.adapter = TabsAdapter(supportFragmentManager)
binding.tabs.setupWithViewPager(binding.viewpager, false)
addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_about, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem) =
when (menuItem.itemId) {
R.id.show_website -> {
showWebsite()
true
setContent {
MdcTheme {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onNavigateUp() }) {
Icon(
Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.navigate_up)
)
}
},
title = {
Text(stringResource(R.string.navigation_drawer_about))
},
actions = {
IconButton(onClick = {
val context = this@AboutActivity
UiUtils.launchUri(context, App.homepageUrl(context))
}) {
Icon(
Icons.Default.Home,
contentDescription = stringResource(R.string.navigation_drawer_website)
)
}
}
)
}
else -> false
}
})
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
val scope = rememberCoroutineScope()
val state = rememberPagerState(pageCount = { 3 })
fun showWebsite() {
UiUtils.launchUri(this, App.homepageUrl(this))
TabRow(state.currentPage) {
Tab(state.currentPage == 0, onClick = {
scope.launch { state.scrollToPage(0) }
}) {
Text(
stringResource(R.string.app_name),
modifier = Modifier.padding(8.dp)
)
}
Tab(state.currentPage == 1, onClick = {
scope.launch { state.scrollToPage(1) }
}) {
Text(
stringResource(R.string.about_translations),
modifier = Modifier.padding(8.dp)
)
}
Tab(state.currentPage == 2, onClick = {
scope.launch { state.scrollToPage(2) }
}) {
Text(
stringResource(R.string.about_libraries),
modifier = Modifier.padding(8.dp)
)
}
}
HorizontalPager(state, modifier = Modifier.padding(8.dp)) { index ->
when (index) {
0 -> AboutApp(licenseInfoProvider = licenseInfoProvider.getOrNull())
1 -> {
val translations = model.translations.observeAsState(emptyList())
TranslatorsGallery(translations.value)
}
2 -> LibrariesContainer(Modifier.fillMaxSize())
}
}
}
}
}
}
}
private inner class TabsAdapter(
fm: FragmentManager
): FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class Model(application: Application) : AndroidViewModel(application) {
override fun getCount() = 3
data class Translation(
val language: String,
val translators: Set<String>
)
override fun getPageTitle(position: Int): String =
when (position) {
0 -> getString(R.string.app_name)
1 -> getString(R.string.about_translations)
else -> getString(R.string.about_libraries)
val translations = MutableLiveData<List<Translation>>()
init {
viewModelScope.launch(Dispatchers.IO) {
loadTranslations()
}
}
private fun loadTranslations() {
val context = getApplication<Application>()
try {
context.resources.assets.open("translators.json").use { stream ->
val jsonTranslations = JSONObject(IOUtils.toString(stream, Charsets.UTF_8))
val result = LinkedList<Translation>()
for (langCode in jsonTranslations.keys()) {
val jsonTranslators = jsonTranslations.getJSONArray(langCode)
val translators = Array<String>(jsonTranslators.length()) { idx ->
jsonTranslators.getString(idx)
}
val langTag = langCode.replace('_', '-')
val language = Locale.forLanguageTag(langTag).displayName
result += Translation(language, translators.toSet())
}
// sort translations by localized language name
val collator = Collator.getInstance()
result.sortWith { o1, o2 ->
collator.compare(o1.language, o2.language)
}
translations.postValue(result)
}
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't load translators", e)
}
}
override fun getItem(position: Int) =
when (position) {
0 -> AppFragment()
1 -> LanguagesFragment()
else -> LibsFragment()
}
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LicenseFragment
interface AppLicenseInfoProvider {
@Composable
fun LicenseInfo()
}
@Module
@InstallIn(ActivityComponent::class)
abstract class LicenseFragmentModule {
interface AppLicenseInfoProviderModule {
@BindsOptionalOf
@LicenseFragment
abstract fun licenseFragment(): Fragment
fun appLicenseInfoProvider(): AppLicenseInfoProvider
}
@AndroidEntryPoint
class AppFragment : Fragment() {
private var _binding: AboutBinding? = null
private val binding get() = _binding!!
val model by viewModels<TextFileModel>()
@Inject
@LicenseFragment
lateinit var licenseFragment: Optional<Fragment>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = AboutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.appName.setText(R.string.app_name)
binding.appVersion.text = getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
binding.buildTime.text = getString(R.string.about_build_date, SimpleDateFormat.getDateInstance().format(BuildConfig.buildTime))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
binding.icon.setImageDrawable(resources.getDrawableForDensity(R.mipmap.ic_launcher, DisplayMetrics.DENSITY_XXXHIGH, null))
binding.pixels.text = HtmlCompat.fromHtml(pixelsHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
if (true /* open-source version */) {
binding.warranty.setText(R.string.about_license_info_no_warranty)
model.initialize("gplv3.html", true)
model.htmlText.observe(viewLifecycleOwner, { spanned ->
binding.licenseText.text = spanned
})
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
class LanguagesFragment: Fragment() {
private var _binding: AboutLanguagesBinding? = null
private val binding get() = _binding!!
val model by viewModels<TranslationsModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = AboutLanguagesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
model.initialize("translators.json", false)
model.translations.observe(viewLifecycleOwner, { translations ->
binding.translators.adapter = TranslationsAdapter(translations)
})
binding.translators.layoutManager = LinearLayoutManager(requireActivity())
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
class TranslationsAdapter(
val translations: List<TranslationsModel.Translation>
): RecyclerView.Adapter<TranslationsAdapter.ViewHolder>() {
private lateinit var binding: AboutTranslationBinding
class ViewHolder(
val context: Context, val binding: AboutTranslationBinding
): RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding = AboutTranslationBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(parent.context, binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val translation = translations[position]
holder.binding.apply {
language.text = translation.language
val profiles = translation.translators.map { "<a href='https://www.transifex.com/user/profile/$it'>$it</a>" }
translators.text = HtmlCompat.fromHtml(
holder.context.getString(R.string.about_translations_thanks, profiles.joinToString(", ")),
HtmlCompat.FROM_HTML_MODE_COMPACT)
translators.movementMethod = LinkMovementMethod.getInstance()
}
}
override fun getItemCount() = translations.size
}
}
class LibsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
ComposeView(requireContext()).apply {
setContent {
MdcTheme {
LibrariesContainer(
Modifier.fillMaxSize()
)
}
}
}
}
open class TextFileModel(
application: Application
): AndroidViewModel(application) {
private var initialized = false
val htmlText = MutableLiveData<Spanned>()
val plainText = MutableLiveData<String>()
@UiThread
fun initialize(assetName: String, html: Boolean) {
if (initialized) return
initialized = true
viewModelScope.launch(Dispatchers.IO) {
getApplication<Application>().resources.assets.open(assetName).use {
val raw = IOUtils.toString(it, Charsets.UTF_8)
if (html) {
val spanned = HtmlCompat.fromHtml(raw, HtmlCompat.FROM_HTML_MODE_LEGACY)
htmlText.postValue(spanned)
} else
plainText.postValue(raw)
}
}
}
}
class TranslationsModel(
application: Application
): TextFileModel(application) {
class Translation(
val language: String,
val translators: Array<String>
@Composable
fun AboutApp(licenseInfoProvider: AboutActivity.AppLicenseInfoProvider? = null) {
Column(
Modifier
.padding(8.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())) {
Image(
UiUtils.adaptiveIconPainterResource(R.mipmap.ic_launcher),
contentDescription = stringResource(R.string.app_name),
modifier = Modifier
.size(128.dp)
.align(Alignment.CenterHorizontally)
)
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.h5,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
val translations = object: MediatorLiveData<List<Translation>>() {
init {
addSource(plainText) { rawJson ->
try {
// parse JSON
val jsonTranslations = JSONObject(rawJson)
val result = LinkedList<Translation>()
for (langCode in jsonTranslations.keys()) {
val jsonTranslators = jsonTranslations.getJSONArray(langCode)
val translators = Array<String>(jsonTranslators.length()) {
idx -> jsonTranslators.getString(idx)
}
Text(
stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
val buildTime = LocalDateTime.ofEpochSecond(BuildConfig.buildTime / 1000, 0, ZoneOffset.UTC)
val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
Text(
stringResource(R.string.about_build_date, dateFormatter.format(buildTime)),
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
val langTag = langCode.replace('_', '-')
val language = Locale.forLanguageTag(langTag).displayName
result += Translation(language, translators)
}
Text(
stringResource(R.string.about_copyright),
style = MaterialTheme.typography.body1,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
// sort translations by localized language name
val collator = Collator.getInstance()
result.sortWith { o1, o2 ->
collator.compare(o1.language, o2.language)
}
postValue(result)
} catch (e: JSONException) {
Logger.log.log(Level.SEVERE, "Could not parse translators JSON", e)
}
}
}
}
PixelBoxes(
arrayOf(Color(0xFFFCF434), Color.White, Color(0xFF9C59D1), Color.Black),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(8.dp)
)
licenseInfoProvider?.LicenseInfo()
}
}
@Composable
@Preview
fun AboutApp_Preview() {
AboutApp(licenseInfoProvider = object : AboutActivity.AppLicenseInfoProvider {
@Composable
override fun LicenseInfo() {
Text("Some flavored License Info")
}
})
}
@Composable
fun TranslatorsGallery(
translations: List<AboutActivity.Model.Translation>,
modifier: Modifier = Modifier
) {
val collator = Collator.getInstance()
LazyColumn (modifier) {
items(translations) { translation ->
Text(
translation.language,
style = MaterialTheme.typography.h6
)
Text(
translation.translators
.sortedWith { a, b -> collator.compare(a, b) }
.joinToString(" · "),
style = MaterialTheme.typography.body1
)
Divider(Modifier.padding(vertical = 8.dp))
}
}
}
@Composable
@Preview
fun TranslatorsGallery_Sample() {
TranslatorsGallery(listOf(
AboutActivity.Model.Translation("Some Language", setOf("User 1", "User 2")),
AboutActivity.Model.Translation("Another Language", setOf("User 3", "User 4"))
))
}

View file

@ -10,20 +10,29 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.Typeface
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.text.Spanned
import android.text.style.StyleSpan
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsClient
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.core.content.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.text.getSpans
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@ -46,6 +55,19 @@ object UiUtils {
const val SHORTCUT_SYNC_ALL = "syncAllAccounts"
const val SNACKBAR_LENGTH_VERY_LONG = 5000 // 5s
@Composable
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
val context = LocalContext.current
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val adaptiveIcon = ResourcesCompat.getDrawable(context.resources, id, null) as? AdaptiveIconDrawable
if (adaptiveIcon != null)
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
else
painterResource(id)
} else
painterResource(id)
}
fun haveCustomTabs(context: Context) = CustomTabsClient.getPackageName(context, null, false) != null
/**

View file

@ -0,0 +1,37 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.widget
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun PixelBoxes(
colors: Array<Color>,
modifier: Modifier = Modifier
) {
Row(modifier) {
for (color in colors)
Box(Modifier.padding(4.dp)) {
Box(Modifier
.background(color)
.size(12.dp))
}
}
}
@Composable
@Preview
fun PixelBoxes_Sample() {
PixelBoxes(arrayOf(Color.Magenta, Color.Yellow, Color.Cyan))
}

View file

@ -1,88 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_horizontal"
style="@style/Theme.MaterialComponents.Light">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin"
android:orientation="vertical"
android:gravity="center_horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="128dp"
android:layout_height="128dp"
android:scaleType="fitXY"
app:srcCompat="@mipmap/ic_launcher"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:text="@string/app_name" />
<TextView
android:id="@+id/app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:layout_marginTop="4dp"
style="@style/TextAppearance.MaterialComponents.Subtitle2"
android:text="@string/about_version" />
<TextView
android:id="@+id/build_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:textAlignment="center"
style="@style/TextAppearance.MaterialComponents.Subtitle2"
android:text="@string/about_build_date" />
<TextView
android:id="@+id/copyright"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/about_copyright"
android:textAlignment="viewStart" />
<TextView
android:id="@+id/warranty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/pixels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAlignment="center" />
<FrameLayout
android:id="@+id/license_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/license_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
</ScrollView>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/translators"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_margin" />

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:contentPadding="@dimen/card_padding"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/language"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Sample Language"
style="@style/TextAppearance.MaterialComponents.Headline6"/>
<TextView
android:id="@+id/translators"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_margin_title_text"
style="@style/TextAppearance.AppCompat.Body1"
tools:text="user1, user2, user3"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="?attr/actionBarPopupTheme" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TabLayout.Colored" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/show_website"
android:icon="@drawable/ic_home"
android:title="@string/navigation_drawer_website"
app:showAsAction="ifRoom" />
</menu>

View file

@ -15,6 +15,7 @@
<string name="field_required">This field is required</string>
<string name="help">Help</string>
<string name="manage_accounts">Manage accounts</string>
<string name="navigate_up">Navigate up</string>
<string name="share">Share</string>
<string name="no_internet_sync_scheduled">No internet, scheduling sync</string>

View file

@ -4,7 +4,9 @@
package at.bitfire.davdroid
import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.intro.IntroFragmentFactory
import at.bitfire.davdroid.ui.intro.OpenSourceFragment
@ -14,17 +16,23 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
@Module
@InstallIn(SingletonComponent::class)
abstract class OseFlavorModule {
interface OseFlavorModules {
//// navigation drawer handler ////
@Module
@InstallIn(ActivityComponent::class)
interface AccountsDrawerHandlerModule {
@Binds
abstract fun accountsDrawerHandler(handler: OseAccountsDrawerHandler): AccountsDrawerHandler
}
@Binds
abstract fun accountsDrawerHandler(handler: OseAccountsDrawerHandler): AccountsDrawerHandler
@Module
@InstallIn(ActivityComponent::class)
interface OpenSourceLicenseInfoProviderModule {
@Binds
fun appLicenseInfoProviderModule(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
}
//// intro fragments ////
@ -33,23 +41,23 @@ abstract class OseFlavorModule {
@Module
@InstallIn(ActivityComponent::class)
abstract class OpenSourceFragmentModule {
interface OpenSourceFragmentModule {
@Binds @IntoSet
abstract fun getFactory(factory: OpenSourceFragment.Factory): IntroFragmentFactory
fun getFactory(factory: OpenSourceFragment.Factory): IntroFragmentFactory
}
@Module
@InstallIn(ActivityComponent::class)
abstract class PermissionsIntroFragmentModule {
interface PermissionsIntroFragmentModule {
@Binds @IntoSet
abstract fun getFactory(factory: PermissionsIntroFragment.Factory): IntroFragmentFactory
fun getFactory(factory: PermissionsIntroFragment.Factory): IntroFragmentFactory
}
@Module
@InstallIn(ActivityComponent::class)
abstract class TasksIntroFragmentModule {
interface TasksIntroFragmentModule {
@Binds @IntoSet
abstract fun getFactory(factory: TasksIntroFragment.Factory): IntroFragmentFactory
fun getFactory(factory: TasksIntroFragment.Factory): IntroFragmentFactory
}
}

View file

@ -0,0 +1,68 @@
package at.bitfire.davdroid.ui
import android.app.Application
import android.text.Spanned
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.text.HtmlCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import javax.inject.Inject
class OpenSourceLicenseInfoProvider @Inject constructor(): AboutActivity.AppLicenseInfoProvider {
@Composable
override fun LicenseInfo() {
LicenseInfoGpl()
}
@Composable
fun LicenseInfoGpl(
model: Model = viewModel()
) {
model.gpl.observeAsState().value?.let { gpl ->
OpenSourceLicenseInfo(gpl.toAnnotatedString())
}
}
@HiltViewModel
class Model @Inject constructor(app: Application): AndroidViewModel(app) {
val gpl = MutableLiveData<Spanned>()
init {
viewModelScope.launch(Dispatchers.IO) {
app.resources.assets.open("gplv3.html").use { inputStream ->
val raw = IOUtils.toString(inputStream, Charsets.UTF_8)
val html = HtmlCompat.fromHtml(raw, HtmlCompat.FROM_HTML_MODE_LEGACY)
gpl.postValue(html)
}
}
}
}
}
@Composable
fun OpenSourceLicenseInfo(license: AnnotatedString) {
Text(license)
}
@Composable
@Preview
fun OpenSourceLicenseInfo_Preview() {
OpenSourceLicenseInfo(AnnotatedString("It's open-source."))
}