mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fixing Settings sticky header text overlapping with menu items (#928)
## Description - Adding a background for the Settings stickyHeader. This helps with visibility and prevents overlap with the menu items when scrolling. ### Related issues In the Settings screen, the header can overlap the text of the menu items when scrolling down. This is a minor UI bug only and does not affect functionality at all, but the fix seemed easy and harmless so I figured I'd give it a go. ### Testing Tested manually on TCL model 50S434 running Android TV. Tested manually using AVD emulation on Television (1080p) and Television (4K) preset devices. Reproduced the problem on all 3 devices and confirmed that my fix works across all. ## Screenshots Before: <img width="1920" height="1080" alt="before-settings-fix" src="https://github.com/user-attachments/assets/15f9916d-2583-4dc4-8302-cb1f3a395b1a" /> After: <img width="1920" height="1080" alt="after-settings-fix" src="https://github.com/user-attachments/assets/3f8781aa-7063-4eb4-bf4a-95ff34b9b1a1" /> ## AI or LLM usage None --------- Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
This commit is contained in:
parent
7939fffd48
commit
55b148971e
1 changed files with 290 additions and 286 deletions
|
|
@ -11,8 +11,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -166,308 +168,310 @@ fun PreferencesContent(
|
|||
LaunchedEffect(Unit) {
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
LazyColumn(
|
||||
state = state,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
|
||||
) {
|
||||
stickyHeader {
|
||||
Text(
|
||||
text = stringResource(screenTitle),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
if (UpdateChecker.ACTIVE &&
|
||||
preferenceScreenOption == PreferenceScreenOption.BASIC &&
|
||||
preferences.autoCheckForUpdates &&
|
||||
updateAvailable
|
||||
Text(
|
||||
text = stringResource(screenTitle),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
)
|
||||
LazyColumn(
|
||||
state = state,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
val updateFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
if (focusedIndex.first == 0 && focusedIndex.second == 0) {
|
||||
// Only re-focus if the user hasn't moved
|
||||
updateFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.install_update),
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
},
|
||||
summary = release?.version?.toString(),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(updateFocusRequester)
|
||||
.playSoundOnFocus(movementSounds),
|
||||
)
|
||||
}
|
||||
}
|
||||
prefList.forEachIndexed { groupIndex, group ->
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(group.title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
val groupPreferences =
|
||||
group.preferences +
|
||||
group.conditionalPreferences
|
||||
.filter { it.condition.invoke(preferences) }
|
||||
.map { it.preferences }
|
||||
.flatten()
|
||||
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||
pref as AppPreference<AppPreferences, Any>
|
||||
if (UpdateChecker.ACTIVE &&
|
||||
preferenceScreenOption == PreferenceScreenOption.BASIC &&
|
||||
preferences.autoCheckForUpdates &&
|
||||
updateAvailable
|
||||
) {
|
||||
item {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
LaunchedEffect(focused) {
|
||||
if (focused) {
|
||||
focusedIndex = Pair(groupIndex, prefIndex)
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
onFocus.invoke(groupIndex, prefIndex)
|
||||
val updateFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
if (focusedIndex.first == 0 && focusedIndex.second == 0) {
|
||||
// Only re-focus if the user hasn't moved
|
||||
updateFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
when (pref) {
|
||||
AppPreference.InstalledVersion -> {
|
||||
var clickCount by remember { mutableIntStateOf(0) }
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.installed_version),
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
if (clickCount++ >= 2) {
|
||||
clickCount = 0
|
||||
viewModel.navigationManager.navigateTo(Destination.Debug)
|
||||
}
|
||||
},
|
||||
summary = installedVersion.toString(),
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.install_update),
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
},
|
||||
summary = release?.version?.toString(),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(updateFocusRequester)
|
||||
.playSoundOnFocus(movementSounds),
|
||||
)
|
||||
}
|
||||
}
|
||||
prefList.forEachIndexed { groupIndex, group ->
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(group.title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
val groupPreferences =
|
||||
group.preferences +
|
||||
group.conditionalPreferences
|
||||
.filter { it.condition.invoke(preferences) }
|
||||
.map { it.preferences }
|
||||
.flatten()
|
||||
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||
pref as AppPreference<AppPreferences, Any>
|
||||
item {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
LaunchedEffect(focused) {
|
||||
if (focused) {
|
||||
focusedIndex = Pair(groupIndex, prefIndex)
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
onFocus.invoke(groupIndex, prefIndex)
|
||||
}
|
||||
}
|
||||
|
||||
AppPreference.Update -> {
|
||||
ClickPreference(
|
||||
title =
|
||||
if (release != null && updateAvailable) {
|
||||
stringResource(R.string.install_update)
|
||||
} else if (!preferences.autoCheckForUpdates && release == null) {
|
||||
stringResource(R.string.check_for_updates)
|
||||
} else {
|
||||
stringResource(R.string.no_update_available)
|
||||
when (pref) {
|
||||
AppPreference.InstalledVersion -> {
|
||||
var clickCount by remember { mutableIntStateOf(0) }
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.installed_version),
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
if (clickCount++ >= 2) {
|
||||
clickCount = 0
|
||||
viewModel.navigationManager.navigateTo(Destination.Debug)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
if (release != null && updateAvailable) {
|
||||
release?.let {
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
summary = installedVersion.toString(),
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.Update -> {
|
||||
ClickPreference(
|
||||
title =
|
||||
if (release != null && updateAvailable) {
|
||||
stringResource(R.string.install_update)
|
||||
} else if (!preferences.autoCheckForUpdates && release == null) {
|
||||
stringResource(R.string.check_for_updates)
|
||||
} else {
|
||||
stringResource(R.string.no_update_available)
|
||||
},
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
if (release != null && updateAvailable) {
|
||||
release?.let {
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
}
|
||||
} else {
|
||||
updateVM.init(preferences.updateUrl)
|
||||
}
|
||||
} else {
|
||||
updateVM.init(preferences.updateUrl)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
},
|
||||
summary =
|
||||
if (updateAvailable) {
|
||||
release?.version?.toString()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.ClearImageCache -> {
|
||||
val summary =
|
||||
remember(cacheUsage) {
|
||||
cacheUsage.let {
|
||||
val diskMB = it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||
val memoryUsedMB =
|
||||
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
||||
val memoryMaxMB =
|
||||
it.imageMemoryMax / AppPreference.MEGA_BIT
|
||||
"Disk: ${diskMB}mb, Memory: ${memoryUsedMB}mb/${memoryMaxMB}mb"
|
||||
}
|
||||
}
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
SingletonImageLoader.get(context).let {
|
||||
it.memoryCache?.clear()
|
||||
it.diskCache?.clear()
|
||||
updateCache = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = summary,
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.UserPinnedNavDrawerItems -> {
|
||||
NavDrawerPreference(
|
||||
title = stringResource(pref.title),
|
||||
summary = pref.summary(context, null),
|
||||
modifier = Modifier,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.SendAppLogs -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.sendAppLogs()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
SubtitleSettings.Reset -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.resetSubtitleSettings()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.RequireProfilePin -> {
|
||||
SwitchPreference(
|
||||
title = stringResource(pref.title),
|
||||
value = currentUser?.pin.isNotNullOrBlank(),
|
||||
onClick = {
|
||||
showPinFlow = true
|
||||
},
|
||||
summaryOn = stringResource(R.string.enabled),
|
||||
summaryOff = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.SeerrIntegration -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
seerrDialogMode =
|
||||
when (val conn = seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
|
||||
}
|
||||
|
||||
SeerrConnectionStatus.NotConfigured -> {
|
||||
SeerrDialogMode.Add
|
||||
}
|
||||
|
||||
is SeerrConnectionStatus.Success -> {
|
||||
SeerrDialogMode.Remove(
|
||||
conn.current.server.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary =
|
||||
when (seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
|
||||
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
|
||||
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
|
||||
onLongClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
},
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
summary =
|
||||
if (updateAvailable) {
|
||||
release?.version?.toString()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.QuickConnect -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
if (currentUser != null) {
|
||||
viewModel.resetQuickConnectStatus()
|
||||
showQuickConnectDialog = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = viewModel.navigationManager::navigateTo,
|
||||
onValueChange = { newValue ->
|
||||
val validation = pref.validate(newValue)
|
||||
when (validation) {
|
||||
is PreferenceValidation.Invalid -> {
|
||||
// TODO?
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
validation.message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
AppPreference.ClearImageCache -> {
|
||||
val summary =
|
||||
remember(cacheUsage) {
|
||||
cacheUsage.let {
|
||||
val diskMB = it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||
val memoryUsedMB =
|
||||
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
||||
val memoryMaxMB =
|
||||
it.imageMemoryMax / AppPreference.MEGA_BIT
|
||||
"Disk: ${diskMB}mb, Memory: ${memoryUsedMB}mb/${memoryMaxMB}mb"
|
||||
}
|
||||
}
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
SingletonImageLoader.get(context).let {
|
||||
it.memoryCache?.clear()
|
||||
it.diskCache?.clear()
|
||||
updateCache = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = summary,
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
PreferenceValidation.Valid -> {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
preferences =
|
||||
viewModel.preferenceDataStore.updateData { prefs ->
|
||||
pref.setter(prefs, newValue)
|
||||
}
|
||||
AppPreference.UserPinnedNavDrawerItems -> {
|
||||
NavDrawerPreference(
|
||||
title = stringResource(pref.title),
|
||||
summary = pref.summary(context, null),
|
||||
modifier = Modifier,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.SendAppLogs -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.sendAppLogs()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
SubtitleSettings.Reset -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.resetSubtitleSettings()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.RequireProfilePin -> {
|
||||
SwitchPreference(
|
||||
title = stringResource(pref.title),
|
||||
value = currentUser?.pin.isNotNullOrBlank(),
|
||||
onClick = {
|
||||
showPinFlow = true
|
||||
},
|
||||
summaryOn = stringResource(R.string.enabled),
|
||||
summaryOff = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.SeerrIntegration -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
seerrDialogMode =
|
||||
when (val conn = seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
|
||||
}
|
||||
|
||||
SeerrConnectionStatus.NotConfigured -> {
|
||||
SeerrDialogMode.Add
|
||||
}
|
||||
|
||||
is SeerrConnectionStatus.Success -> {
|
||||
SeerrDialogMode.Remove(
|
||||
conn.current.server.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary =
|
||||
when (seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
|
||||
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
|
||||
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
|
||||
},
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
AppPreference.QuickConnect -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
if (currentUser != null) {
|
||||
viewModel.resetQuickConnectStatus()
|
||||
showQuickConnectDialog = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
preference = pref,
|
||||
value = value,
|
||||
onNavigate = viewModel.navigationManager::navigateTo,
|
||||
onValueChange = { newValue ->
|
||||
val validation = pref.validate(newValue)
|
||||
when (validation) {
|
||||
is PreferenceValidation.Invalid -> {
|
||||
// TODO?
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
validation.message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
PreferenceValidation.Valid -> {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
preferences =
|
||||
viewModel.preferenceDataStore.updateData { prefs ->
|
||||
pref.setter(prefs, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue