mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 11:39:15 +00:00
Rewrite About activity to Compose (bitfireAT/davx5#432)
This commit is contained in:
parent
cf9340107f
commit
4a200dfbb7
|
@ -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'
|
||||
|
|
|
@ -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"))
|
||||
))
|
||||
}
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
37
app/src/main/kotlin/at/bitfire/davdroid/ui/widget/Boxes.kt
Normal file
37
app/src/main/kotlin/at/bitfire/davdroid/ui/widget/Boxes.kt
Normal 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))
|
||||
}
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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."))
|
||||
}
|
Loading…
Reference in a new issue