mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-10-05 18:43:24 -04:00
Add an onboarding card for updates
This commit is contained in:
15 files changed
+142
-64
No files matched your search
@@ -30,6 +30,7 @@ class MyAppsScreenshotTest(localeName: String) : LocalizedScreenshotTest(localeN
|
||||
appUpdates = getUpdates(localeList),
|
||||
installingApps = emptyList(),
|
||||
installedApps = getInstalledApps(localeList),
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = false,
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ class OnboardingManager
|
||||
constructor(@param:ApplicationContext private val context: Context) {
|
||||
|
||||
private companion object {
|
||||
const val KEY_MY_APPS_UPDATES = "myAppsUpdates"
|
||||
const val KEY_FILTER = "appFilter"
|
||||
const val KEY_REPO_LIST = "repoList"
|
||||
const val KEY_REPO_DETAILS = "repoDetails"
|
||||
@@ -26,6 +27,9 @@ constructor(@param:ApplicationContext private val context: Context) {
|
||||
|
||||
private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE)
|
||||
|
||||
private val _showMyAppsUpdatesHint = Onboarding(KEY_MY_APPS_UPDATES, prefs)
|
||||
val showMyAppsUpdatesHint = _showMyAppsUpdatesHint.flow
|
||||
|
||||
private val _showFilterOnboarding = Onboarding(KEY_FILTER, prefs)
|
||||
val showFilterOnboarding = _showFilterOnboarding.flow
|
||||
|
||||
@@ -38,6 +42,10 @@ constructor(@param:ApplicationContext private val context: Context) {
|
||||
private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs)
|
||||
val showAppIssueHint = _showAppIssueHint.flow
|
||||
|
||||
fun onMyAppsUpdateHintSeen() {
|
||||
_showMyAppsUpdatesHint.onSeen(prefs)
|
||||
}
|
||||
|
||||
fun onFilterOnboardingSeen() {
|
||||
_showFilterOnboarding.onSeen(prefs)
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ fun MyAppsLoadingPreview() {
|
||||
installingApps = emptyList(),
|
||||
appUpdates = null,
|
||||
installedApps = null,
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
@@ -226,6 +227,7 @@ fun MyAppsEmptyPreview() {
|
||||
installingApps = emptyList(),
|
||||
appUpdates = emptyList(),
|
||||
installedApps = emptyList(),
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
|
||||
@@ -15,6 +15,7 @@ data class MyAppsModel(
|
||||
val installingApps: List<InstallingAppItem>,
|
||||
val appsWithIssue: List<AppWithIssueItem>? = null,
|
||||
val installedApps: List<InstalledAppItem>? = null,
|
||||
val showUpdatesHint: Boolean,
|
||||
val showAppIssueHint: Boolean,
|
||||
val sortOrder: AppListSortOrder = AppListSortOrder.NAME,
|
||||
val networkState: NetworkState,
|
||||
@@ -32,6 +33,8 @@ interface MyAppsActions {
|
||||
|
||||
fun ignoreAppIssue(item: AppWithIssueItem)
|
||||
|
||||
fun onUpdatesHintSeen()
|
||||
|
||||
fun onAppIssueHintSeen()
|
||||
|
||||
fun exportInstalledApps()
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.fdroid.database.NotAvailable
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.MeteredConnectionDialog
|
||||
import org.fdroid.ui.utils.OfflineBar
|
||||
import org.fdroid.ui.utils.OnboardingCard
|
||||
import org.fdroid.ui.utils.getMyAppsInfo
|
||||
import org.fdroid.ui.utils.myAppsModel
|
||||
|
||||
@@ -74,7 +75,17 @@ fun MyAppsList(
|
||||
modifier.then(if (currentPackageName == null) Modifier else Modifier.selectableGroup()),
|
||||
) {
|
||||
// Updates header with Update all button
|
||||
if (!updatableApps.isNullOrEmpty()) {
|
||||
if (updatableApps != null && updatableApps.isEmpty() && myAppsInfo.model.showUpdatesHint) {
|
||||
item(key = "A-hint", contentType = "hint") {
|
||||
OnboardingCard(
|
||||
title = stringResource(R.string.apps_updates_hint_title),
|
||||
message = stringResource(R.string.apps_updates_hint_text),
|
||||
buttonText = stringResource(R.string.hide),
|
||||
onGotIt = { myAppsInfo.actions.onUpdatesHintSeen() },
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
} else if (!updatableApps.isNullOrEmpty()) {
|
||||
if (!myAppsInfo.model.networkState.isOnline) {
|
||||
item(key = "OfflineBar", contentType = "offlineBar") { OfflineBar() }
|
||||
}
|
||||
@@ -211,12 +222,8 @@ fun MyAppsList(
|
||||
NotAvailableDialog(app.packageName) { showNotAvailableDialog = false }
|
||||
}
|
||||
}
|
||||
// Installed apps header (only show when we have non-empty lists above)
|
||||
val aboveNonEmpty =
|
||||
installingApps.isNotEmpty() ||
|
||||
!updatableApps.isNullOrEmpty() ||
|
||||
!appsWithIssue.isNullOrEmpty()
|
||||
if (aboveNonEmpty && !installedApps.isNullOrEmpty()) {
|
||||
// List of installed apps
|
||||
if (!installedApps.isNullOrEmpty()) {
|
||||
item(key = "D", contentType = "header") {
|
||||
Text(
|
||||
text = stringResource(R.string.installed_apps__activity_title),
|
||||
@@ -224,9 +231,6 @@ fun MyAppsList(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
// List of installed apps
|
||||
if (installedApps != null)
|
||||
items(items = installedApps, key = { it.packageName }, contentType = { "D" }) { app ->
|
||||
val isSelected = app.packageName == currentPackageName
|
||||
val interactionModifier =
|
||||
@@ -241,6 +245,7 @@ fun MyAppsList(
|
||||
val modifier = Modifier.animateItem().then(interactionModifier)
|
||||
InstalledAppRow(app, isSelected, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
val meteredLambda = showMeteredDialog
|
||||
if (meteredLambda != null)
|
||||
@@ -264,3 +269,24 @@ private fun MyAppsListPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
private fun Preview() {
|
||||
FDroidContent {
|
||||
MyApps(
|
||||
myAppsInfo =
|
||||
getMyAppsInfo(
|
||||
myAppsModel.copy(
|
||||
appUpdates = emptyList(),
|
||||
installingApps = emptyList(),
|
||||
appsWithIssue = emptyList(),
|
||||
)
|
||||
),
|
||||
currentPackageName = null,
|
||||
onAppItemClick = {},
|
||||
onNav = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ fun MyAppsPresenter(
|
||||
appInstallStatesFlow: StateFlow<Map<String, InstallState>>,
|
||||
appsWithIssuesFlow: StateFlow<List<AppWithIssueItem>?>,
|
||||
installedAppsFlow: Flow<List<InstalledAppItem>>,
|
||||
showUpdatesHintFlow: StateFlow<Boolean>,
|
||||
showAppIssueHintFlow: StateFlow<Boolean>,
|
||||
searchQueryFlow: StateFlow<String>,
|
||||
sortOrderFlow: StateFlow<AppListSortOrder>,
|
||||
@@ -36,52 +37,48 @@ fun MyAppsPresenter(
|
||||
|
||||
// we want to show apps currently installing/updating even if they have updates available,
|
||||
// so we need to handle those first
|
||||
val installingApps =
|
||||
appInstallStates.mapNotNull { (packageName, state) ->
|
||||
if (state is InstallStateWithInfo) {
|
||||
val keep =
|
||||
searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
if (keep) {
|
||||
processedPackageNames.add(packageName)
|
||||
InstallingAppItem(packageName, state)
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val updates =
|
||||
appUpdates?.filter {
|
||||
val installingApps = appInstallStates.mapNotNull { (packageName, state) ->
|
||||
if (state is InstallStateWithInfo) {
|
||||
val keep =
|
||||
if (searchQuery.isBlank()) {
|
||||
it.packageName !in processedPackageNames
|
||||
} else {
|
||||
it.packageName !in processedPackageNames &&
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
if (keep) processedPackageNames.add(it.packageName)
|
||||
keep
|
||||
searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
if (keep) {
|
||||
processedPackageNames.add(packageName)
|
||||
InstallingAppItem(packageName, state)
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val withIssues =
|
||||
appsWithIssues?.filter {
|
||||
val keep =
|
||||
if (searchQuery.isBlank()) {
|
||||
it.packageName !in processedPackageNames
|
||||
} else {
|
||||
it.packageName !in processedPackageNames &&
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
if (keep) processedPackageNames.add(it.packageName)
|
||||
keep
|
||||
}
|
||||
val installed =
|
||||
installedApps?.filter {
|
||||
}
|
||||
val updates = appUpdates?.filter {
|
||||
val keep =
|
||||
if (searchQuery.isBlank()) {
|
||||
it.packageName !in processedPackageNames
|
||||
} else {
|
||||
it.packageName !in processedPackageNames &&
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
if (keep) processedPackageNames.add(it.packageName)
|
||||
keep
|
||||
}
|
||||
val withIssues = appsWithIssues?.filter {
|
||||
val keep =
|
||||
if (searchQuery.isBlank()) {
|
||||
it.packageName !in processedPackageNames
|
||||
} else {
|
||||
it.packageName !in processedPackageNames &&
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
if (keep) processedPackageNames.add(it.packageName)
|
||||
keep
|
||||
}
|
||||
val installed = installedApps?.filter {
|
||||
if (searchQuery.isBlank()) {
|
||||
it.packageName !in processedPackageNames
|
||||
} else {
|
||||
it.packageName !in processedPackageNames &&
|
||||
it.name.normalize().contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
var updateBytes: Long? = 0L
|
||||
updates?.forEach {
|
||||
val size = it.update.size
|
||||
@@ -102,6 +99,9 @@ fun MyAppsPresenter(
|
||||
appUpdates = updates?.sort(sortOrder),
|
||||
appsWithIssue = withIssues?.sort(sortOrder),
|
||||
installedApps = installed?.sort(sortOrder),
|
||||
showUpdatesHint =
|
||||
showUpdatesHintFlow.collectAsState().value &&
|
||||
installingApps.none { it.installState.showProgress },
|
||||
showAppIssueHint = showAppIssueHintFlow.collectAsState().value,
|
||||
sortOrder = sortOrder,
|
||||
networkState = networkStateFlow.collectAsState().value,
|
||||
|
||||
@@ -99,6 +99,7 @@ constructor(
|
||||
appInstallStatesFlow = appInstallManager.appInstallStates,
|
||||
appsWithIssuesFlow = updatesManager.appsWithIssues,
|
||||
installedAppsFlow = installedAppItems,
|
||||
showUpdatesHintFlow = onboardingManager.showMyAppsUpdatesHint,
|
||||
showAppIssueHintFlow = onboardingManager.showAppIssueHint,
|
||||
searchQueryFlow = searchQuery,
|
||||
sortOrderFlow = sortOrder,
|
||||
@@ -139,6 +140,8 @@ constructor(
|
||||
updatesManager.loadUpdates()
|
||||
}
|
||||
|
||||
override fun onUpdatesHintSeen() = onboardingManager.onMyAppsUpdateHintSeen()
|
||||
|
||||
override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen()
|
||||
|
||||
override fun exportInstalledApps() {
|
||||
|
||||
@@ -61,7 +61,7 @@ import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.search.TopSearchBar
|
||||
import org.fdroid.ui.utils.BackButton
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
import org.fdroid.ui.utils.OnboardingCard
|
||||
import org.fdroid.ui.utils.OnboardingPopupCard
|
||||
import org.fdroid.ui.utils.TopAppBarButton
|
||||
import org.fdroid.ui.utils.appListItems
|
||||
import org.fdroid.ui.utils.getAppListInfo
|
||||
@@ -81,7 +81,7 @@ fun AppList(
|
||||
|
||||
val hintController = rememberHintController(overlay = getHintOverlayColor())
|
||||
val hint = rememberHint {
|
||||
OnboardingCard(
|
||||
OnboardingPopupCard(
|
||||
title = stringResource(R.string.onboarding_app_list_filter_title),
|
||||
message = stringResource(R.string.onboarding_app_list_filter_message),
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.fdroid.R
|
||||
import org.fdroid.download.NetworkState
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
import org.fdroid.ui.utils.OnboardingCard
|
||||
import org.fdroid.ui.utils.OnboardingPopupCard
|
||||
import org.fdroid.ui.utils.getHintOverlayColor
|
||||
import org.fdroid.ui.utils.getRepositoriesInfo
|
||||
import org.fdroid.ui.utils.repoItems
|
||||
@@ -54,7 +54,7 @@ fun Repositories(info: RepositoryInfo, isBigScreen: Boolean, onBackClicked: () -
|
||||
val hintController = rememberHintController(overlay = getHintOverlayColor())
|
||||
val hint = rememberHint {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
OnboardingCard(
|
||||
OnboardingPopupCard(
|
||||
title = stringResource(R.string.repo_list_info_title),
|
||||
message = stringResource(R.string.repo_list_info_text),
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.BackButton
|
||||
import org.fdroid.ui.utils.BigLoadingIndicator
|
||||
import org.fdroid.ui.utils.MeteredConnectionDialog
|
||||
import org.fdroid.ui.utils.OnboardingCard
|
||||
import org.fdroid.ui.utils.OnboardingPopupCard
|
||||
import org.fdroid.ui.utils.TopAppBarButton
|
||||
import org.fdroid.ui.utils.TopAppBarOverflowButton
|
||||
import org.fdroid.ui.utils.getHintOverlayColor
|
||||
@@ -58,7 +58,7 @@ fun RepoDetails(
|
||||
val hintController = rememberHintController(overlay = getHintOverlayColor())
|
||||
val hint = rememberHint {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
OnboardingCard(
|
||||
OnboardingPopupCard(
|
||||
title = stringResource(R.string.repo_details),
|
||||
message = stringResource(R.string.repo_details_info_text),
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.fdroid.ui.utils
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
@@ -20,16 +23,38 @@ import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.R
|
||||
import org.fdroid.ui.FDroidContent
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun OnboardingPopupCard(
|
||||
title: String,
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonText: String = stringResource(R.string.got_it),
|
||||
onGotIt: () -> Unit = {},
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OnboardingCard(
|
||||
title = title,
|
||||
message = message,
|
||||
modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth),
|
||||
buttonText = buttonText,
|
||||
buttonModifier = Modifier.focusRequester(focusRequester),
|
||||
onGotIt = onGotIt,
|
||||
)
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun OnboardingCard(
|
||||
title: String,
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonText: String = stringResource(R.string.got_it),
|
||||
buttonModifier: Modifier = Modifier,
|
||||
onGotIt: () -> Unit = {},
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
ElevatedCard(modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth)) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
@@ -40,15 +65,15 @@ fun OnboardingCard(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
TextButton(
|
||||
onClick = onGotIt,
|
||||
modifier =
|
||||
Modifier.focusRequester(focusRequester).padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Text(text = stringResource(R.string.got_it))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(
|
||||
onClick = onGotIt,
|
||||
modifier = buttonModifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Text(text = buttonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@@ -407,6 +407,8 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo =
|
||||
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {}
|
||||
|
||||
override fun onUpdatesHintSeen() {}
|
||||
|
||||
override fun onAppIssueHintSeen() {}
|
||||
|
||||
override fun exportInstalledApps() {}
|
||||
@@ -495,6 +497,7 @@ internal val myAppsModel =
|
||||
lastUpdated = System.currentTimeMillis() - DAYS.toMillis(3),
|
||||
),
|
||||
),
|
||||
showUpdatesHint = true,
|
||||
showAppIssueHint = true,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<string name="onboarding_app_list_filter_message">Use filters to only show apps from specific categories or repositories. You can also change the sort order.</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="hide">Hide</string>
|
||||
|
||||
<string name="category_group_productivity">Productivity</string>
|
||||
<string name="category_group_tools">Tools</string>
|
||||
@@ -295,6 +296,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="app_list__dismiss_downloading_app">Download canceled</string>
|
||||
|
||||
<string name="installed_apps__activity_title">Installed apps</string>
|
||||
<string name="apps_updates_hint_title">Congratulations 🥳</string>
|
||||
<string name="apps_updates_hint_text">All your apps are up to date</string>
|
||||
<!-- Should be exactly the same as the standard word for "Share" throughout Android and apps -->
|
||||
<string name="send_installed_apps">Share installed apps</string>
|
||||
<string name="send_installed_apps_csv">Apps installed by F-Droid as CSV file</string>
|
||||
|
||||
@@ -22,6 +22,7 @@ fun MyAppsOnlyInstalledAppsAndNoInternet() {
|
||||
installingApps = emptyList(),
|
||||
appsWithIssue = null,
|
||||
installedApps = getInstalledApps(),
|
||||
showUpdatesHint = true,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
@@ -46,6 +47,7 @@ fun MyAppsInstalledAndIssues() {
|
||||
installingApps = emptyList(),
|
||||
appsWithIssue = getAppIssues(),
|
||||
installedApps = getInstalledApps(),
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = true,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
@@ -70,6 +72,7 @@ fun MyAppsUpdatesOnlyAndNoInternet() {
|
||||
installingApps = emptyList(),
|
||||
appsWithIssue = null,
|
||||
installedApps = emptyList(),
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.LAST_UPDATED,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
@@ -119,6 +122,7 @@ fun MyAppsFullList() {
|
||||
installingApps = installingApps,
|
||||
appsWithIssue = getAppIssues(),
|
||||
installedApps = getInstalledApps(),
|
||||
showUpdatesHint = false,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:037f0d24d270c8fb52ac88bff5f40c999ea11035d03e270b6fd7968ad24506a5
|
||||
size 67807
|
||||
oid sha256:d915f7d6abf21e98310c1806371c071eb04e62d9104a5f6c06d933f1fffbb237
|
||||
size 88468
|
||||
Reference in new issue
Block a user