diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ddcfb415..5bba530a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -119,10 +119,6 @@
android:label="@string/activity_edit_calendar"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="stateAlwaysHidden" />
-
diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/InfoActivity.kt b/app/src/main/java/at/bitfire/icsdroid/ui/InfoActivity.kt
deleted file mode 100644
index 48708682..00000000
--- a/app/src/main/java/at/bitfire/icsdroid/ui/InfoActivity.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
- */
-
-package at.bitfire.icsdroid.ui
-
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import android.widget.Toast
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.core.graphics.drawable.toBitmap
-import androidx.core.net.toUri
-import androidx.core.text.HtmlCompat
-import androidx.datastore.preferences.core.edit
-import at.bitfire.icsdroid.BuildConfig
-import at.bitfire.icsdroid.Constants
-import at.bitfire.icsdroid.R
-import at.bitfire.icsdroid.Settings.Companion.nextReminder
-import at.bitfire.icsdroid.dataStore
-import at.bitfire.icsdroid.service.ComposableStartupService
-import at.bitfire.icsdroid.service.ComposableStartupService.Companion.FLAG_DONATION_DIALOG
-import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar
-import at.bitfire.icsdroid.ui.partials.GenericAlertDialog
-import at.bitfire.icsdroid.ui.theme.setContentThemed
-import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
-import com.mikepenz.aboutlibraries.ui.compose.rememberLibraries
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.runBlocking
-import java.util.ServiceLoader
-
-@AndroidEntryPoint
-class InfoActivity: AppCompatActivity() {
-
- /**
- * Will get updated when the activity is created.
- * Stores whether the donate dialog should be shown or not.
- */
- private var hasDonateDialogService: Boolean = false
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // Init and collect all ComposableStartupServices
- val compStartupServices = ServiceLoader.load(ComposableStartupService::class.java)
- .also { srv ->
- hasDonateDialogService = srv.any { it.hasFlag(FLAG_DONATION_DIALOG) }
- }
-
- setContentThemed {
- compStartupServices.forEach { service ->
- val show: Boolean by service.shouldShow()
- if (show) service.Content()
- }
-
- MainLayout()
- }
- }
-
- private fun showWebSite() {
- launchUri("https://icsx5.bitfire.at/?pk_campaign=icsx5-app&pk_kwd=info-activity".toUri())
- }
-
- private fun showMastodon() {
- launchUri("https://fosstodon.org/@davx5app".toUri())
- }
-
- private fun launchUri(uri: Uri) {
- val intent = Intent(Intent.ACTION_VIEW, uri)
- try {
- startActivity(intent)
- } catch (_: ActivityNotFoundException) {
- Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_LONG).show()
- Log.w(Constants.TAG, "No browser to view $uri")
- }
- }
-
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- @Preview
- fun MainLayout() {
- Scaffold(
- topBar = {
- ExtendedTopAppBar(
- navigationIcon = {
- IconButton({ onNavigateUp() }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = null
- )
- }
- },
- title = {
- Text(
- stringResource(R.string.app_name)
- )
- },
- actions = {
- IconButton({ showWebSite() }) {
- Icon(
- painter = painterResource(R.drawable.ic_public),
- contentDescription = stringResource(R.string.app_info_web_site)
- )
- }
- IconButton({ showMastodon() }) {
- Icon(
- painter = painterResource(R.drawable.mastodon_white),
- contentDescription = stringResource(R.string.app_info_mastodon)
- )
- }
- }
- )
- }
- ) { contentPadding ->
- val libraries by rememberLibraries {
- resources.openRawResource(R.raw.aboutlibraries).bufferedReader().use { input ->
- input.readText()
- }
- }
-
- Column(Modifier.padding(contentPadding)) {
- Header()
- License()
- LibrariesContainer(
- libraries = libraries
- )
- }
- }
- }
-
- @Composable
- fun Header() {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- val context = LocalContext.current
-
- Image(
- bitmap = context.applicationInfo
- .loadIcon(context.packageManager)
- .toBitmap()
- .asImageBitmap(),
- contentDescription = null,
- modifier = Modifier
- .padding(vertical = 12.dp)
- .size(72.dp)
- )
- Text(
- text = stringResource(R.string.app_name),
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.onBackground
- )
- Text(
- text = stringResource(
- R.string.app_info_version,
- BuildConfig.VERSION_NAME,
- BuildConfig.FLAVOR
- ),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onBackground,
- fontWeight = FontWeight.Normal
- )
- Text(
- text = stringResource(R.string.app_info_copyright),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onBackground,
- fontWeight = FontWeight.Normal,
- textAlign = TextAlign.Center,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp)
- )
- }
- }
-
- @Composable
- fun License() {
- val showLicenseDialog = rememberSaveable { mutableStateOf(false) }
- if (showLicenseDialog.value)
- TextDialog(R.string.app_info_gplv3_note, showLicenseDialog)
-
- val showDonateDialog = rememberSaveable { mutableStateOf(false) }
- if (showDonateDialog.value)
- TextDialog(R.string.donate_message, showDonateDialog)
-
- Row {
- OutlinedButton(
- onClick = { showLicenseDialog.value = true },
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 4.dp)
- ) {
- Text(stringResource(R.string.app_info_gplv3))
- }
- OutlinedButton(
- onClick = {
- if (hasDonateDialogService) runBlocking {
- // If there's a donate dialog service, show the dialog
- dataStore.edit { it[nextReminder] = 0 }
- } else {
- // If there's no service, show the donate dialog directly
- showDonateDialog.value = true
- }
- },
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 4.dp)
- ) {
- Text(stringResource(R.string.app_info_donate))
- }
- }
- }
-
- @Composable
- fun TextDialog(@StringRes text: Int, state: MutableState) {
- GenericAlertDialog(
- content = { Text(HtmlCompat.fromHtml(
- getString(text).replace("\n", "
"),
- HtmlCompat.FROM_HTML_MODE_COMPACT).toString()) },
- confirmButton = stringResource(R.string.edit_calendar_dismiss) to {
- state.value = false
- },
- ) { state.value = false }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/nav/Destination.kt b/app/src/main/java/at/bitfire/icsdroid/ui/nav/Destination.kt
index 04c330d3..8be1d7d4 100644
--- a/app/src/main/java/at/bitfire/icsdroid/ui/nav/Destination.kt
+++ b/app/src/main/java/at/bitfire/icsdroid/ui/nav/Destination.kt
@@ -13,6 +13,9 @@ sealed interface Destination : NavKey {
@Serializable
object SubscriptionList : Destination
+ @Serializable
+ object Info : Destination
+
@Serializable
data class AddSubscription(
val title: String? = null,
diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/nav/MainApp.kt b/app/src/main/java/at/bitfire/icsdroid/ui/nav/MainApp.kt
index 16440f79..2ad02ade 100644
--- a/app/src/main/java/at/bitfire/icsdroid/ui/nav/MainApp.kt
+++ b/app/src/main/java/at/bitfire/icsdroid/ui/nav/MainApp.kt
@@ -28,6 +28,7 @@ import at.bitfire.icsdroid.MainActivity.Companion.EXTRA_THROWABLE
import at.bitfire.icsdroid.service.ComposableStartupService
import at.bitfire.icsdroid.ui.partials.AlertDialog
import at.bitfire.icsdroid.ui.screen.AddSubscriptionScreen
+import at.bitfire.icsdroid.ui.screen.InfoScreen
import at.bitfire.icsdroid.ui.screen.SubscriptionsScreen
import java.util.ServiceLoader
@@ -94,9 +95,16 @@ fun MainApp(
entry(Destination.SubscriptionList) {
SubscriptionsScreen(
requestPermissions,
+ onAboutRequested = { backStack.add(Destination.Info) },
onAddRequested = { backStack.add(Destination.AddSubscription()) }
)
}
+ entry(Destination.Info) {
+ InfoScreen(
+ compStartupServices,
+ onBackRequested = ::goBack
+ )
+ }
entry { destination ->
var url: String? = null
LaunchedEffect(intentExtras) {
diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/InfoScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/InfoScreen.kt
new file mode 100644
index 00000000..ff05e9fe
--- /dev/null
+++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/InfoScreen.kt
@@ -0,0 +1,231 @@
+package at.bitfire.icsdroid.ui.screen
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalResources
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
+import androidx.datastore.preferences.core.edit
+import at.bitfire.icsdroid.BuildConfig
+import at.bitfire.icsdroid.R
+import at.bitfire.icsdroid.Settings.Companion.nextReminder
+import at.bitfire.icsdroid.dataStore
+import at.bitfire.icsdroid.service.ComposableStartupService
+import at.bitfire.icsdroid.service.ComposableStartupService.Companion.FLAG_DONATION_DIALOG
+import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar
+import at.bitfire.icsdroid.ui.partials.GenericAlertDialog
+import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
+import com.mikepenz.aboutlibraries.ui.compose.rememberLibraries
+import kotlinx.coroutines.runBlocking
+import java.util.ServiceLoader
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun InfoScreen(
+ compStartupServices: ServiceLoader,
+ onBackRequested: () -> Unit
+) {
+ val resources = LocalResources.current
+ val uriHandler = LocalUriHandler.current
+
+ val hasDonateDialogService = compStartupServices.any { it.hasFlag(FLAG_DONATION_DIALOG) }
+
+ Scaffold(
+ topBar = {
+ ExtendedTopAppBar(
+ navigationIcon = {
+ IconButton(onBackRequested) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text(
+ stringResource(R.string.app_name)
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ uriHandler.openUri("https://icsx5.bitfire.at/?pk_campaign=icsx5-app&pk_kwd=info-activity")
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_public),
+ contentDescription = stringResource(R.string.app_info_web_site)
+ )
+ }
+ IconButton(
+ onClick = {
+ uriHandler.openUri("https://fosstodon.org/@davx5app")
+ }
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.mastodon_white),
+ contentDescription = stringResource(R.string.app_info_mastodon)
+ )
+ }
+ }
+ )
+ }
+ ) { contentPadding ->
+ val libraries by rememberLibraries {
+ resources.openRawResource(R.raw.aboutlibraries).bufferedReader().use { input ->
+ input.readText()
+ }
+ }
+
+ Column(Modifier.padding(contentPadding)) {
+ Header()
+ License(hasDonateDialogService)
+ LibrariesContainer(
+ libraries = libraries
+ )
+ }
+ }
+}
+
+@Composable
+private fun Header() {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val context = LocalContext.current
+
+ Image(
+ bitmap = context.applicationInfo
+ .loadIcon(context.packageManager)
+ .toBitmap()
+ .asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(vertical = 12.dp)
+ .size(72.dp)
+ )
+ Text(
+ text = stringResource(R.string.app_name),
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ Text(
+ text = stringResource(
+ R.string.app_info_version,
+ BuildConfig.VERSION_NAME,
+ BuildConfig.FLAVOR
+ ),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontWeight = FontWeight.Normal
+ )
+ Text(
+ text = stringResource(R.string.app_info_copyright),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontWeight = FontWeight.Normal,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ )
+ }
+}
+
+@Composable
+private fun License(
+ hasDonateDialogService: Boolean
+) {
+ val context = LocalContext.current
+
+ val showLicenseDialog = rememberSaveable { mutableStateOf(false) }
+ if (showLicenseDialog.value)
+ TextDialog(R.string.app_info_gplv3_note, showLicenseDialog)
+
+ val showDonateDialog = rememberSaveable { mutableStateOf(false) }
+ if (showDonateDialog.value)
+ TextDialog(R.string.donate_message, showDonateDialog)
+
+ Row {
+ OutlinedButton(
+ onClick = { showLicenseDialog.value = true },
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ ) {
+ Text(stringResource(R.string.app_info_gplv3))
+ }
+ OutlinedButton(
+ onClick = {
+ if (hasDonateDialogService) runBlocking {
+ // If there's a donate dialog service, show the dialog
+ context.dataStore.edit { it[nextReminder] = 0 }
+ } else {
+ // If there's no service, show the donate dialog directly
+ showDonateDialog.value = true
+ }
+ },
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 4.dp)
+ ) {
+ Text(stringResource(R.string.app_info_donate))
+ }
+ }
+}
+
+@Composable
+private fun TextDialog(@StringRes text: Int, state: MutableState) {
+ GenericAlertDialog(
+ content = {
+ val htmlString = stringResource(text).replace("\n", "
")
+ val linkStyle = LocalTextStyle.current.copy(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ ).toSpanStyle()
+ Text(
+ text = AnnotatedString.fromHtml(
+ htmlString,
+ linkStyles = TextLinkStyles(linkStyle)
+ )
+ )
+ },
+ confirmButton = stringResource(R.string.edit_calendar_dismiss) to {
+ state.value = false
+ },
+ ) { state.value = false }
+}
diff --git a/app/src/main/java/at/bitfire/icsdroid/ui/screen/SubscriptionsScreen.kt b/app/src/main/java/at/bitfire/icsdroid/ui/screen/SubscriptionsScreen.kt
index 02d035d6..cc9ad25b 100644
--- a/app/src/main/java/at/bitfire/icsdroid/ui/screen/SubscriptionsScreen.kt
+++ b/app/src/main/java/at/bitfire/icsdroid/ui/screen/SubscriptionsScreen.kt
@@ -57,7 +57,6 @@ import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.UriUtils
import at.bitfire.icsdroid.db.entity.Subscription
import at.bitfire.icsdroid.model.SubscriptionsModel
-import at.bitfire.icsdroid.ui.InfoActivity
import at.bitfire.icsdroid.ui.partials.ActionCard
import at.bitfire.icsdroid.ui.partials.CalendarListItem
import at.bitfire.icsdroid.ui.partials.ExtendedTopAppBar
@@ -68,6 +67,7 @@ import at.bitfire.icsdroid.ui.views.EditSubscriptionActivity
@Composable
fun SubscriptionsScreen(
requestPermissions: Boolean,
+ onAboutRequested: () -> Unit,
onAddRequested: () -> Unit,
model: SubscriptionsModel = hiltViewModel()
) {
@@ -96,9 +96,7 @@ fun SubscriptionsScreen(
SubscriptionsScreen(
model = model,
- onAboutRequested = {
- activity?.startActivity(Intent(context, InfoActivity::class.java))
- },
+ onAboutRequested = onAboutRequested,
onAddRequested = onAddRequested,
onRequestCalendarPermissions = requestCalendarPermissions,
onRequestNotificationPermission = requestNotificationPermission,