diff --git a/app/src/main/java/chat/revolt/components/screens/settings/AttributionItem.kt b/app/src/main/java/chat/revolt/components/screens/settings/AttributionItem.kt new file mode 100644 index 00000000..8c987b5d --- /dev/null +++ b/app/src/main/java/chat/revolt/components/screens/settings/AttributionItem.kt @@ -0,0 +1,45 @@ +package chat.revolt.components.screens.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +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.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import chat.revolt.R +import chat.revolt.screens.about.Library + +@Composable +fun AttributionItem( + library: Library, + onClick: () -> Unit +) { + SelectionContainer { + Column( + modifier = Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = library.name, + style = MaterialTheme.typography.bodyLarge, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(id = R.string.oss_attribution_tap_to_view_license), + style = MaterialTheme.typography.bodySmall, + ) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/about/AttributionScreen.kt b/app/src/main/java/chat/revolt/screens/about/AttributionScreen.kt index ad9c99de..49861207 100644 --- a/app/src/main/java/chat/revolt/screens/about/AttributionScreen.kt +++ b/app/src/main/java/chat/revolt/screens/about/AttributionScreen.kt @@ -1,20 +1,149 @@ package chat.revolt.screens.about +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +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.safeDrawingPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.revolt.R import chat.revolt.components.generic.PageHeader -import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults +import chat.revolt.components.screens.settings.AttributionItem +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +@Serializable +data class AboutLibraries( + val metadata: Metadata, + val libraries: List, + val licenses: Map +) + +@Serializable +data class Metadata( + val generated: String, +) + +@Serializable +data class License( + val content: String? = null, + val hash: String, + val internalHash: String? = null, + val url: String, + val spdxId: String? = null, + val name: String, +) + +@Serializable +data class Library( + val uniqueId: String, + val funding: JsonArray, + val developers: List, + val artifactVersion: String, + val description: String, + val scm: Scm? = null, + val name: String, + val licenses: List, + val website: String? = null, + val organization: Organization? = null, +) + +@Serializable +data class Organization( + val url: String, + val name: String, +) + +@Serializable +data class Developer( + val organisationUrl: String? = null, + val name: String? = null, +) + +@Serializable +data class Scm( + val connection: String? = null, + val url: String, + val developerConnection: String? = null, +) + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AttributionScreen(navController: NavController) { + var libraries by remember { mutableStateOf(null) } + + val context = LocalContext.current + + var licenceSheetOpen by remember { mutableStateOf(false) } + var licenseSheetTarget by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + context.resources.openRawResource(R.raw.aboutlibraries).use { stream -> + val text = stream.bufferedReader().use { it.readText() } + libraries = Json.decodeFromString(AboutLibraries.serializer(), text) + } + } + + if (licenceSheetOpen) { + val licenceSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + sheetState = licenceSheetState, + onDismissRequest = { + licenceSheetOpen = false + } + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + Text( + text = licenseSheetTarget, + style = MaterialTheme.typography.headlineMedium + ) + + Spacer(modifier = Modifier.height(8.dp)) + + libraries?.let { + val license = it.licenses[licenseSheetTarget] + if (license != null) { + Text(text = license.content ?: "No license content found.") + } else { + Text(text = "No license found.") + } + } + } + } + } + Column( modifier = Modifier .safeDrawingPadding() @@ -24,17 +153,59 @@ fun AttributionScreen(navController: NavController) { showBackButton = true, onBackButtonClicked = { navController.popBackStack() }) - LibrariesContainer( - modifier = Modifier - .fillMaxSize() - .weight(1f), - colors = LibraryDefaults.libraryColors( - backgroundColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - badgeBackgroundColor = MaterialTheme.colorScheme.primary, - badgeContentColor = MaterialTheme.colorScheme.onPrimary - ) - ) + libraries?.let { + LazyColumn { + item { + Column( + modifier = Modifier + .padding(16.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.oss_attribution_body) + ) + Text( + text = stringResource(R.string.oss_attribution_body_2) + ) + Text( + text = stringResource(R.string.oss_attribution_warning), + color = MaterialTheme.colorScheme.error + ) + + Text( + text = stringResource( + R.string.oss_attribution_generation_date, + libraries?.metadata?.generated ?: "" + ), + color = LocalContentColor.current.copy(alpha = 0.6f) + ) + } + } + + items( + items = it.libraries.sortedBy { library -> library.name } + ) { library -> + AttributionItem(library = library) { + licenceSheetOpen = true + licenseSheetTarget = library.licenses.first() + } + } + + item(key = "cat") { + Text( + text = "🐈", + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + textAlign = TextAlign.Center + ) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9322bf81..46c8444a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,7 +72,13 @@ About Revolt on Android + OSS Attribution + Revolt is built with the help of these awesome open-source projects. + We\'re grateful to the developers of these projects for making their work available to the world. + The list below is automatically generated. It may be incomplete or inaccurate. + Last updated: %1$s + Tap to view license Sorry, this feature is not ready yet.