# HG changeset patch # User Da Risk # Date 1745362902 14400 # Node ID d59fc19f19fa6aa7cc31e02c54d0fdf92142b527 # Parent 59b612ba364452fc946ac1b301038360371db798 ui:material3: convert to kotlin-multiplatform diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/build.gradle.kts --- a/ui/material3/build.gradle.kts Tue Apr 22 18:48:58 2025 -0400 +++ b/ui/material3/build.gradle.kts Tue Apr 22 19:01:42 2025 -0400 @@ -19,10 +19,13 @@ * You should have received a copy of the GNU General Public License * along with AboutOss. If not, see . */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") - kotlin("android") + kotlin("multiplatform") alias(libs.plugins.org.jetbrains.kotlin.compose.compiler) + alias(libs.plugins.org.jetbrains.compose.multiplatform) id("com.geekorum.build.source-license-checker") `maven-publish` } @@ -30,6 +33,44 @@ group = "com.geekorum.aboutoss" version = "0.1.0" +kotlin { + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "aboutoss-ui-material3" + isStatic = true + } + } + + sourceSets { + commonMain.dependencies { + api(project(":ui:common")) + implementation(project(":core")) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.org.jetbrains.androidx.navigation.compose) + } + + androidMain.dependencies { + api(libs.androidx.activity) + implementation(dependencies.platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + } + } +} + + android { namespace = "com.geekorum.aboutoss.ui.material3" compileSdk = 35 @@ -55,11 +96,8 @@ } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { @@ -75,12 +113,6 @@ } dependencies { - api(project(":ui:common")) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.navigation.compose) - testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.espresso.core) diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/androidMain/AndroidManifest.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/material3/src/androidMain/AndroidManifest.xml Tue Apr 22 19:01:42 2025 -0400 @@ -0,0 +1,33 @@ + + + + + + + \ No newline at end of file diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/androidMain/kotlin/OpenSourceLicensesActivity.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/material3/src/androidMain/kotlin/OpenSourceLicensesActivity.kt Tue Apr 22 19:01:42 2025 -0400 @@ -0,0 +1,130 @@ +/* + * AboutOss is an utility library to retrieve and display + * opensource licenses in Android applications. + * + * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. + * + * This file is part of AboutOss. + * + * AboutOss is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AboutOss is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AboutOss. If not, see . + */ +package com.geekorum.aboutoss.ui.material3 + +import android.app.Activity +import android.net.Uri +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.geekorum.aboutoss.core.gms.GmsLicenseInfoRepository +import com.geekorum.aboutoss.ui.common.BaseOpensourceLicenseActivity +import com.geekorum.aboutoss.ui.common.Factory +import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel +import com.geekorum.aboutoss.ui.material3.OpenSourceLicensesActivity.Companion.themeProvider +import kotlinx.coroutines.Dispatchers + +/** + * Activity to display opensource license information + * + * This activity use Material compose to create the UI. + * You can specify the Material theme to use by setting [themeProvider] + * before launching the activity + */ +open class OpenSourceLicensesActivity : BaseOpensourceLicenseActivity() { + override val viewModel: OpenSourceLicensesViewModel by viewModels( + factoryProducer = { + val gmsLicenseInfoRepository = GmsLicenseInfoRepository( + appContext = applicationContext, + mainCoroutineDispatcher = Dispatchers.Main, + ioCoroutineDispatcher = Dispatchers.IO, + ) + OpenSourceLicensesViewModel.Factory(gmsLicenseInfoRepository) + } + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + themeProvider { + DependencyNavHost( + openSourceLicensesViewModel = viewModel, + navigateUp = { + if (!onNavigateUp()) { + finish() + } + } + ) + } + } + } + + companion object { + /** + * The composable Theme function to set the theme of the UI in [OpenSourceLicensesActivity] + * Default to base material theme [MaterialTheme] + */ + var themeProvider: @Composable (@Composable () -> Unit) -> Unit = { content -> + val darkTheme: Boolean = isSystemInDarkTheme() + val colorScheme = MaterialTheme.colorScheme + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + MaterialTheme(content = content) + } + } +} + +@Composable +fun DependencyNavHost( + openSourceLicensesViewModel: OpenSourceLicensesViewModel, + navigateUp: () -> Unit +) { + val navController = rememberNavController() + NavHost(navController, startDestination = "dependencies") { + composable("dependencies") { + OpenSourceDependenciesListScreen( + viewModel = openSourceLicensesViewModel, + onDependencyClick = { + navController.navigate("dependency_license/${Uri.encode(it)}") + }, + onUpClick = navigateUp + ) + } + composable("dependency_license/{dependency}") { + val dependency = requireNotNull(it.arguments?.getString("dependency")) + OpenSourceLicenseScreen( + viewModel = openSourceLicensesViewModel, + dependency = dependency, + onUpClick = { + navController.popBackStack() + }, + ) + } + } +} diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/OpenSourceDependenciesListScreen.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/OpenSourceDependenciesListScreen.kt Tue Apr 22 19:01:42 2025 -0400 @@ -0,0 +1,126 @@ +/* + * AboutOss is an utility library to retrieve and display + * opensource licenses in Android applications. + * + * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. + * + * This file is part of AboutOss. + * + * AboutOss is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AboutOss is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AboutOss. If not, see . + */ +package com.geekorum.aboutoss.ui.material3 + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.geekorum.aboutoss.common.generated.resources.title_oss_licenses +import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel +import org.jetbrains.compose.resources.stringResource +import com.geekorum.aboutoss.common.generated.resources.Res as CommonRes + +/** + * Display the list of dependencies used in the application + * + * @param viewModel the [OpenSourceLicensesViewModel] to use + * @param onDependencyClick lambda to execute on click on one dependency item + * @param onUpClick lambda to execute on click on the up arrow + */ +@Composable +fun OpenSourceDependenciesListScreen( + viewModel: OpenSourceLicensesViewModel, + onDependencyClick: (String) -> Unit, + onUpClick: () -> Unit +) { + val dependencies by viewModel.dependenciesList.collectAsState(initial = emptyList()) + OpenSourceDependenciesListScreen( + dependencies = dependencies, + onDependencyClick = onDependencyClick, + onUpClick = onUpClick + ) +} + +/** + * Display the list of dependencies used in the application + * + * @param dependencies the list of dependencies + * @param onDependencyClick lambda to execute on click on one dependency item + * @param onUpClick lambda to execute on click on the up arrow + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OpenSourceDependenciesListScreen( + dependencies: List, + onDependencyClick: (String) -> Unit, + onUpClick: () -> Unit +) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text(stringResource(CommonRes.string.title_oss_licenses)) }, + navigationIcon = { + IconButton(onClick = onUpClick) { + Icon( + Icons.Default.ArrowBack, + contentDescription = null + ) + } + }, + scrollBehavior = scrollBehavior + ) + }) { + LazyColumn(Modifier.fillMaxSize(), contentPadding = it) { + items(dependencies) { + Column { + ListItem( + modifier = Modifier + .height(64.dp) + .clickable(onClick = { onDependencyClick(it) }), + headlineContent = { + Text( + it, modifier = Modifier.padding(horizontal = 16.dp), + overflow = TextOverflow.Ellipsis, maxLines = 1 + ) + } + ) + Divider(Modifier.padding(horizontal = 16.dp)) + } + } + } + } +} diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/OpenSourceLicenseScreen.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/OpenSourceLicenseScreen.kt Tue Apr 22 19:01:42 2025 -0400 @@ -0,0 +1,192 @@ +/* + * AboutOss is an utility library to retrieve and display + * opensource licenses in Android applications. + * + * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. + * + * This file is part of AboutOss. + * + * AboutOss is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AboutOss is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AboutOss. If not, see . + */ +package com.geekorum.aboutoss.ui.material3 + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.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.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.UrlAnnotation +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withAnnotation +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel + +/** + * Display the opensource license of a dependency + * + * @param viewModel the [OpenSourceLicensesViewModel] to use + * @param dependency the dependency + * @param onUpClick lambda to execute on click on the up arrow + */ +@Composable +fun OpenSourceLicenseScreen( + viewModel: OpenSourceLicensesViewModel, + dependency: String, + onUpClick: () -> Unit, +) { + val license by viewModel.getLicenseDependency(dependency).collectAsState("") + OpenSourceLicenseScreen( + dependency = dependency, + license = license, + onUpClick = onUpClick, + onUrlClick = { + viewModel.openLinkInBrowser(it) + }, + onUrlsFound = { + viewModel.mayLaunchUrl(*it.toTypedArray()) + } + ) +} + +/** + * Display the opensource license of a dependency + * + * @param dependency the dependency + * @param license the opensource license text + * @param onUpClick lambda to execute on click on the up arrow + * @param onUrlClick lambda to execute on click on a url + * @param onUrlsFound lambda to execute when all urls in the license have been found + */ +@OptIn(ExperimentalLayoutApi::class, ExperimentalTextApi::class, ExperimentalMaterial3Api::class) +@Composable +fun OpenSourceLicenseScreen( + dependency: String, + license: String, + onUpClick: () -> Unit, + onUrlClick: (String) -> Unit, + onUrlsFound: (List) -> Unit, +) { + val linkifiedLicense = linkifyText(text = license) + LaunchedEffect(linkifiedLicense) { + val uris = + linkifiedLicense.getUrlAnnotations(0, linkifiedLicense.length).map { it.item.url } + onUrlsFound(uris) + } + + val scrollState = rememberScrollState() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar(title = { Text(dependency, overflow = TextOverflow.Ellipsis, maxLines = 1) }, + navigationIcon = { + IconButton(onClick = onUpClick) { + Icon( + Icons.Default.ArrowBack, + contentDescription = null + ) + } + }, + ) + }) { paddingValues -> + val layoutResult = remember { mutableStateOf(null) } + val pressIndicator = Modifier.pointerInput(layoutResult, linkifiedLicense) { + detectTapGestures { pos -> + layoutResult.value?.let { layoutResult -> + val posWithScroll = pos.copy(y = pos.y + scrollState.value) + val offset = layoutResult.getOffsetForPosition(posWithScroll) + linkifiedLicense.getUrlAnnotations(start = offset, end = offset) + .firstOrNull()?.let { annotation -> + onUrlClick(annotation.item.url) + } + } + } + } + + Text(linkifiedLicense, + modifier = Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .padding(horizontal = 16.dp) + .fillMaxSize() + .then(pressIndicator) + .verticalScroll(scrollState), + onTextLayout = { + layoutResult.value = it + } + ) + } +} + +/** + * https://regexr.com/37i6s + */ +private val urlRegexp = """https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)""".toRegex() + +@OptIn(ExperimentalTextApi::class) +@Composable +private fun linkifyText(text: String): AnnotatedString { + val style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + return remember(text, style) { + buildAnnotatedString { + var currentIdx = 0 + for (match in urlRegexp.findAll(text)) { + if (currentIdx < match.range.first) { + append(text.substring(currentIdx, match.range.first)) + } + val url = text.substring(match.range) + withAnnotation(UrlAnnotation(url)) { + withStyle(style) { + append(url) + } + } + currentIdx = match.range.last + 1 + } + append(text.substring(currentIdx)) + } + } +} diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/main/AndroidManifest.xml --- a/ui/material3/src/main/AndroidManifest.xml Tue Apr 22 18:48:58 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ - - - - - - - \ No newline at end of file diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceDependenciesListScreen.kt --- a/ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceDependenciesListScreen.kt Tue Apr 22 18:48:58 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ -/* - * AboutOss is an utility library to retrieve and display - * opensource licenses in Android applications. - * - * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. - * - * This file is part of AboutOss. - * - * AboutOss is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AboutOss is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AboutOss. If not, see . - */ -package com.geekorum.aboutoss.ui.material3 - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel -import com.geekorum.aboutoss.ui.common.R as commonR - -/** - * Display the list of dependencies used in the application - * - * @param viewModel the [OpenSourceLicensesViewModel] to use - * @param onDependencyClick lambda to execute on click on one dependency item - * @param onUpClick lambda to execute on click on the up arrow - */ -@Composable -fun OpenSourceDependenciesListScreen( - viewModel: OpenSourceLicensesViewModel, - onDependencyClick: (String) -> Unit, - onUpClick: () -> Unit -) { - val dependencies by viewModel.dependenciesList.collectAsState(initial = emptyList()) - OpenSourceDependenciesListScreen( - dependencies = dependencies, - onDependencyClick = onDependencyClick, - onUpClick = onUpClick - ) -} - -/** - * Display the list of dependencies used in the application - * - * @param dependencies the list of dependencies - * @param onDependencyClick lambda to execute on click on one dependency item - * @param onUpClick lambda to execute on click on the up arrow - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun OpenSourceDependenciesListScreen( - dependencies: List, - onDependencyClick: (String) -> Unit, - onUpClick: () -> Unit -) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { Text(stringResource(commonR.string.title_oss_licenses)) }, - navigationIcon = { - IconButton(onClick = onUpClick) { - Icon( - Icons.Default.ArrowBack, - contentDescription = null - ) - } - }, - scrollBehavior = scrollBehavior - ) - }) { - LazyColumn(Modifier.fillMaxSize(), contentPadding = it) { - items(dependencies) { - Column { - ListItem( - modifier = Modifier - .height(64.dp) - .clickable(onClick = { onDependencyClick(it) }), - headlineContent = { - Text( - it, modifier = Modifier.padding(horizontal = 16.dp), - overflow = TextOverflow.Ellipsis, maxLines = 1 - ) - } - ) - Divider(Modifier.padding(horizontal = 16.dp)) - } - } - } - } -} diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceLicenseScreen.kt --- a/ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceLicenseScreen.kt Tue Apr 22 18:48:58 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,173 +0,0 @@ -/* - * AboutOss is an utility library to retrieve and display - * opensource licenses in Android applications. - * - * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. - * - * This file is part of AboutOss. - * - * AboutOss is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AboutOss is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AboutOss. If not, see . - */ -package com.geekorum.aboutoss.ui.material3 - -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.* -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel - -/** - * Display the opensource license of a dependency - * - * @param viewModel the [OpenSourceLicensesViewModel] to use - * @param dependency the dependency - * @param onUpClick lambda to execute on click on the up arrow - */ -@Composable -fun OpenSourceLicenseScreen( - viewModel: OpenSourceLicensesViewModel, - dependency: String, - onUpClick: () -> Unit, -) { - val context = LocalContext.current - val license by viewModel.getLicenseDependency(dependency).collectAsState("") - OpenSourceLicenseScreen( - dependency = dependency, - license = license, - onUpClick = onUpClick, - onUrlClick = { - viewModel.openLinkInBrowser(context, it) - }, - onUrlsFound = { - val uris = it.map { uri -> uri.toUri() } - viewModel.mayLaunchUrl(*uris.toTypedArray()) - } - ) -} - -/** - * Display the opensource license of a dependency - * - * @param dependency the dependency - * @param license the opensource license text - * @param onUpClick lambda to execute on click on the up arrow - * @param onUrlClick lambda to execute on click on a url - * @param onUrlsFound lambda to execute when all urls in the license have been found - */ -@OptIn(ExperimentalLayoutApi::class, ExperimentalTextApi::class, ExperimentalMaterial3Api::class) -@Composable -fun OpenSourceLicenseScreen( - dependency: String, - license: String, - onUpClick: () -> Unit, - onUrlClick: (String) -> Unit, - onUrlsFound: (List) -> Unit, -) { - val linkifiedLicense = linkifyText(text = license) - LaunchedEffect(linkifiedLicense) { - val uris = - linkifiedLicense.getUrlAnnotations(0, linkifiedLicense.length).map { it.item.url } - onUrlsFound(uris) - } - - val scrollState = rememberScrollState() - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar(title = { Text(dependency, overflow = TextOverflow.Ellipsis, maxLines = 1) }, - navigationIcon = { - IconButton(onClick = onUpClick) { - Icon( - Icons.Default.ArrowBack, - contentDescription = null - ) - } - }, - ) - }) { paddingValues -> - val layoutResult = remember { mutableStateOf(null) } - val pressIndicator = Modifier.pointerInput(layoutResult, linkifiedLicense) { - detectTapGestures { pos -> - layoutResult.value?.let { layoutResult -> - val posWithScroll = pos.copy(y = pos.y + scrollState.value) - val offset = layoutResult.getOffsetForPosition(posWithScroll) - linkifiedLicense.getUrlAnnotations(start = offset, end = offset) - .firstOrNull()?.let { annotation -> - onUrlClick(annotation.item.url) - } - } - } - } - - Text(linkifiedLicense, - modifier = Modifier - .padding(paddingValues) - .consumeWindowInsets(paddingValues) - .padding(horizontal = 16.dp) - .fillMaxSize() - .then(pressIndicator) - .verticalScroll(scrollState), - onTextLayout = { - layoutResult.value = it - } - ) - } -} - -/** - * https://regexr.com/37i6s - */ -private val urlRegexp = """https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)""".toRegex() - -@OptIn(ExperimentalTextApi::class) -@Composable -private fun linkifyText(text: String): AnnotatedString { - val style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ) - return remember(text, style) { - buildAnnotatedString { - var currentIdx = 0 - for (match in urlRegexp.findAll(text)) { - if (currentIdx < match.range.first) { - append(text.substring(currentIdx, match.range.first)) - } - val url = text.substring(match.range) - withAnnotation(UrlAnnotation(url)) { - withStyle(style) { - append(url) - } - } - currentIdx = match.range.last + 1 - } - append(text.substring(currentIdx)) - } - } -} diff -r 59b612ba3644 -r d59fc19f19fa ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceLicensesActivity.kt --- a/ui/material3/src/main/java/com/geekorum/aboutoss/ui/material3/OpenSourceLicensesActivity.kt Tue Apr 22 18:48:58 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -/* - * AboutOss is an utility library to retrieve and display - * opensource licenses in Android applications. - * - * Copyright (C) 2023-2025 by Frederic-Charles Barthelery. - * - * This file is part of AboutOss. - * - * AboutOss is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AboutOss is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AboutOss. If not, see . - */ -package com.geekorum.aboutoss.ui.material3 - -import android.app.Activity -import android.net.Uri -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.geekorum.aboutoss.ui.common.BaseOpensourceLicenseActivity -import com.geekorum.aboutoss.ui.common.OpenSourceLicensesViewModel - -/** - * Activity to display opensource license information - * - * This activity use Material compose to create the UI. - * You can specify the Material theme to use by setting [themeProvider] - * before launching the activity - */ -open class OpenSourceLicensesActivity : BaseOpensourceLicenseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { - themeProvider { - DependencyNavHost( - openSourceLicensesViewModel = viewModel, - navigateUp = { - if (!onNavigateUp()) { - finish() - } - } - ) - } - } - } - - companion object { - /** - * The composable Theme function to set the theme of the UI in [OpenSourceLicensesActivity] - * Default to base material theme [MaterialTheme] - */ - var themeProvider: @Composable (@Composable () -> Unit) -> Unit = { content -> - val darkTheme: Boolean = isSystemInDarkTheme() - val colorScheme = MaterialTheme.colorScheme - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - MaterialTheme(content = content) - } - } -} - -@Composable -fun DependencyNavHost( - openSourceLicensesViewModel: OpenSourceLicensesViewModel, - navigateUp: () -> Unit -) { - val navController = rememberNavController() - NavHost(navController, startDestination = "dependencies") { - composable("dependencies") { - OpenSourceDependenciesListScreen( - viewModel = openSourceLicensesViewModel, - onDependencyClick = { - navController.navigate("dependency_license/${Uri.encode(it)}") - }, - onUpClick = navigateUp - ) - } - composable("dependency_license/{dependency}") { - val dependency = requireNotNull(it.arguments?.getString("dependency")) - OpenSourceLicenseScreen( - viewModel = openSourceLicensesViewModel, - dependency = dependency, - onUpClick = { - navController.popBackStack() - }, - ) - } - } -}