List the open-source projects Calendula ships (#284)
Adds a Settings entry listing the open-source projects Calendula ships, with copyright holder, SPDX licence id and a tap-through to each project's source. Apache-2.0 §4(a)/§4(d) are discharged by what the recipient of the APK is shown, not by a notice in the repository. New `ui/licences/` package mirroring Agendula's: - `OpenSourceLicences.kt` — `Attribution(name, copyright, licence, sourceUrl)` list plus a `Licence` enum (SPDX id + licence URL). Hand-maintained; the KDoc says why, and that adding a dependency means adding it here. - `LicencesScreen.kt` — `CollapsingScaffold` + one `GroupedRow` per entry, summary `<copyright> · <SPDX id>`, tapping a row opens that project's source. Reached from a new row in the **About** group in `SettingsScreen.kt`, before the privacy row so the group still ends on privacy. It uses `Icons.Default.Code`, since Gavel already belongs to the app's own License row. Entries cover everything with `implementation` scope: AndroidX/Compose/Glance/WorkManager/DataStore/DocumentFile, Kotlin and kotlinx, Dagger and Hilt, Material Design icons, and floret-kit (MIT). Test-only dependencies are not shipped and stay off the list. Strings live in `values/strings.xml`. Deviation from the issue: the footer text differs from Agendula's. Agendula's says every change to vendored code is listed in the linked repository — Calendula modifies nothing it ships, so the footer states that instead. Closes #281 Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/284
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.licences
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||||
|
import de.jeanlucmakiola.floret.components.GroupedListInset
|
||||||
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third-party attribution.
|
||||||
|
*
|
||||||
|
* Reachable from Settings rather than buried, because Apache-2.0 §4(a) is about
|
||||||
|
* what the *recipient of the binary* is told. Each row opens the project's source.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun LicencesScreen(onBack: () -> Unit) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
CollapsingScaffold(
|
||||||
|
title = stringResource(R.string.licences_title),
|
||||||
|
onBack = onBack,
|
||||||
|
predictiveBack = true,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.licences_intro),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = GroupedListInset),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OpenSourceLicences.ALL.forEachIndexed { index, attribution ->
|
||||||
|
GroupedRow(
|
||||||
|
title = attribution.name,
|
||||||
|
summary = "${attribution.copyright} · ${attribution.licence.spdxId}",
|
||||||
|
position = positionOf(index, OpenSourceLicences.ALL.size),
|
||||||
|
onClick = { uriHandler.openUri(attribution.sourceUrl) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.licences_footer),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = GroupedListInset),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.licences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The third-party code Calendula ships, and what each licence obliges us to say.
|
||||||
|
*
|
||||||
|
* This screen is a **licence obligation, not an about-page nicety**:
|
||||||
|
* **Apache-2.0 §4(a)** requires the licence to travel with the work and **§4(d)**
|
||||||
|
* propagates any `NOTICE` file. What discharges that is what the recipient of the
|
||||||
|
* APK is shown — a notice only visible to someone reading the repository does not
|
||||||
|
* count. Nearly everything below is Apache-2.0.
|
||||||
|
*
|
||||||
|
* Hand-maintained on purpose. Generators read POM metadata, and POM metadata is
|
||||||
|
* routinely wrong — the licence name is often not an SPDX id and the licence URL
|
||||||
|
* frequently 404s, which produces confidently incomplete output. A short honest
|
||||||
|
* list beats a long generated one that omits the entry that mattered.
|
||||||
|
*
|
||||||
|
* **When adding a dependency, add it here.** There is no automated check that can
|
||||||
|
* tell you that you forgot. Test-only dependencies (JUnit, Truth, Turbine) are not
|
||||||
|
* shipped in the APK and stay off the list.
|
||||||
|
*/
|
||||||
|
data class Attribution(
|
||||||
|
val name: String,
|
||||||
|
val copyright: String,
|
||||||
|
val licence: Licence,
|
||||||
|
/** Where the source can actually be obtained; what each row links to. */
|
||||||
|
val sourceUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class Licence(val spdxId: String, val url: String) {
|
||||||
|
APACHE_2("Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0"),
|
||||||
|
MIT("MIT", "https://opensource.org/license/mit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
object OpenSourceLicences {
|
||||||
|
|
||||||
|
val ALL: List<Attribution> = listOf(
|
||||||
|
Attribution(
|
||||||
|
name = "AndroidX, Jetpack Compose, Glance, WorkManager, DataStore, DocumentFile",
|
||||||
|
copyright = "© The Android Open Source Project",
|
||||||
|
licence = Licence.APACHE_2,
|
||||||
|
sourceUrl = "https://cs.android.com/androidx/platform/frameworks/support",
|
||||||
|
),
|
||||||
|
Attribution(
|
||||||
|
name = "Kotlin, kotlinx.coroutines, kotlinx.datetime",
|
||||||
|
copyright = "© JetBrains s.r.o. and Kotlin Programming Language contributors",
|
||||||
|
licence = Licence.APACHE_2,
|
||||||
|
sourceUrl = "https://github.com/JetBrains/kotlin",
|
||||||
|
),
|
||||||
|
Attribution(
|
||||||
|
name = "Dagger and Hilt",
|
||||||
|
copyright = "© Google LLC and The Dagger Authors",
|
||||||
|
licence = Licence.APACHE_2,
|
||||||
|
sourceUrl = "https://github.com/google/dagger",
|
||||||
|
),
|
||||||
|
Attribution(
|
||||||
|
name = "Material Design icons",
|
||||||
|
copyright = "© Google LLC",
|
||||||
|
licence = Licence.APACHE_2,
|
||||||
|
sourceUrl = "https://github.com/google/material-design-icons",
|
||||||
|
),
|
||||||
|
Attribution(
|
||||||
|
name = "floret-kit",
|
||||||
|
copyright = "© Jean-Luc Makiola",
|
||||||
|
licence = Licence.MIT,
|
||||||
|
sourceUrl = "https://codeberg.org/jlmakiola/floret-kit",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.Backup
|
|||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
import androidx.compose.material.icons.filled.Cake
|
import androidx.compose.material.icons.filled.Cake
|
||||||
import androidx.compose.material.icons.filled.CalendarMonth
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Code
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.Gavel
|
import androidx.compose.material.icons.filled.Gavel
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
@@ -54,6 +55,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
|
import de.jeanlucmakiola.calendula.ui.licences.LicencesScreen
|
||||||
import de.jeanlucmakiola.floret.components.AboutCard
|
import de.jeanlucmakiola.floret.components.AboutCard
|
||||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.floret.components.GroupedListInset
|
import de.jeanlucmakiola.floret.components.GroupedListInset
|
||||||
@@ -67,7 +69,9 @@ import de.jeanlucmakiola.floret.crash.submitCrashReport
|
|||||||
import de.jeanlucmakiola.floret.locale.AppLanguage
|
import de.jeanlucmakiola.floret.locale.AppLanguage
|
||||||
|
|
||||||
/** The settings sub-screens reached from the hub's category rows. */
|
/** The settings sub-screens reached from the hub's category rows. */
|
||||||
private enum class SettingsSection { Appearance, Views, EventForm, Notifications, SpecialDates, Widgets }
|
private enum class SettingsSection {
|
||||||
|
Appearance, Views, EventForm, Notifications, SpecialDates, Widgets, Licences
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings hub: three labelled groups of category rows, each opening a
|
* Settings hub: three labelled groups of category rows, each opening a
|
||||||
@@ -146,6 +150,13 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
WidgetsScreen(state = state, viewModel = viewModel, onBack = { section = null })
|
WidgetsScreen(state = state, viewModel = viewModel, onBack = { section = null })
|
||||||
}
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = section == SettingsSection.Licences,
|
||||||
|
enter = slideInHorizontally(slideSpec) { it } + fadeIn(),
|
||||||
|
exit = slideOutHorizontally(slideSpec) { it } + fadeOut(),
|
||||||
|
) {
|
||||||
|
LicencesScreen(onBack = { section = null })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +264,13 @@ private fun SettingsHub(
|
|||||||
icon = Icons.Default.Gavel,
|
icon = Icons.Default.Gavel,
|
||||||
position = Position.Middle,
|
position = Position.Middle,
|
||||||
)
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_licences),
|
||||||
|
summary = stringResource(R.string.settings_licences_subtitle),
|
||||||
|
position = Position.Middle,
|
||||||
|
leading = { CategoryIcon(Icons.Default.Code, ChipAccent.Neutral) },
|
||||||
|
onClick = { onOpenSection(SettingsSection.Licences) },
|
||||||
|
)
|
||||||
AboutLinkRow(
|
AboutLinkRow(
|
||||||
label = stringResource(R.string.settings_about_privacy),
|
label = stringResource(R.string.settings_about_privacy),
|
||||||
url = stringResource(R.string.about_privacy_url),
|
url = stringResource(R.string.about_privacy_url),
|
||||||
|
|||||||
@@ -620,6 +620,8 @@
|
|||||||
<string name="settings_license_value">MIT</string>
|
<string name="settings_license_value">MIT</string>
|
||||||
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
<string name="settings_about_author">by Jean-Luc Makiola</string>
|
||||||
<string name="settings_about_source">Source</string>
|
<string name="settings_about_source">Source</string>
|
||||||
|
<string name="settings_licences">Open source licenses</string>
|
||||||
|
<string name="settings_licences_subtitle">The projects Calendula is built on</string>
|
||||||
<string name="settings_about_privacy">Privacy policy</string>
|
<string name="settings_about_privacy">Privacy policy</string>
|
||||||
<string name="settings_about_support">Support development</string>
|
<string name="settings_about_support">Support development</string>
|
||||||
<string name="settings_about_version">Version %1$s</string>
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
@@ -627,6 +629,11 @@
|
|||||||
<string name="settings_report_problem">Report a problem</string>
|
<string name="settings_report_problem">Report a problem</string>
|
||||||
<string name="settings_report_problem_hint">Send a crash report or open the issue tracker</string>
|
<string name="settings_report_problem_hint">Send a crash report or open the issue tracker</string>
|
||||||
|
|
||||||
|
<!-- Open source licences -->
|
||||||
|
<string name="licences_title">Open source licenses</string>
|
||||||
|
<string name="licences_intro">Calendula includes the work below. Tap any entry to get its source code.</string>
|
||||||
|
<string name="licences_footer">Every project here is included unmodified, under the license shown.</string>
|
||||||
|
|
||||||
<!-- Calendar manager -->
|
<!-- Calendar manager -->
|
||||||
<string name="calendars_title">Calendars</string>
|
<string name="calendars_title">Calendars</string>
|
||||||
<string name="calendars_local_header">Your calendars</string>
|
<string name="calendars_local_header">Your calendars</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user