Merge pull request #6650 from element-hq/feature/bma/a11yFixes
[a11y] Fix a set of issues
This commit is contained in:
commit
26fe5b6492
44 changed files with 271 additions and 169 deletions
|
|
@ -19,6 +19,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
|
|
@ -90,7 +93,11 @@ fun ChooseSelfVerificationModeView(
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onLearnMore)
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
.semantics {
|
||||
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
|
||||
role = Role.Button
|
||||
},
|
||||
text = stringResource(CommonStrings.action_learn_more),
|
||||
style = ElementTheme.typography.fontBodyLgMedium
|
||||
)
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ private fun SpaceFilterButton(
|
|||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
val isSelected = spaceFiltersState is SpaceFiltersState.Selected
|
||||
IconButton(
|
||||
onClick = ::onClick,
|
||||
|
|
@ -320,7 +321,15 @@ private fun AccountIcon(
|
|||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
|
||||
contentDescription = if (isCurrentAccount) {
|
||||
if (showAvatarIndicator) {
|
||||
stringResource(CommonStrings.a11y_settings_with_required_action)
|
||||
} else {
|
||||
stringResource(CommonStrings.common_settings)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
if (showAvatarIndicator) {
|
||||
RedIndicatorAtom(
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ class MessagesFlowNode(
|
|||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val canUseOverlay: Boolean,
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -227,10 +228,11 @@ class MessagesFlowNode(
|
|||
callback.navigateToRoomDetails()
|
||||
}
|
||||
|
||||
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
|
||||
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
|
||||
return processEventClick(
|
||||
timelineMode = timelineMode,
|
||||
event = event,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +322,11 @@ class MessagesFlowNode(
|
|||
)
|
||||
val callback = object : MediaViewerEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
overlay.hide()
|
||||
if (navTarget.canUseOverlay) {
|
||||
overlay.hide()
|
||||
} else {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewInTimeline(eventId: EventId) {
|
||||
|
|
@ -414,10 +420,11 @@ class MessagesFlowNode(
|
|||
}
|
||||
NavTarget.PinnedMessagesList -> {
|
||||
val callback = object : PinnedMessagesListNode.Callback {
|
||||
override fun handleEventClick(event: TimelineItem.Event) {
|
||||
override fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) {
|
||||
processEventClick(
|
||||
timelineMode = Timeline.Mode.PinnedEvents,
|
||||
event = event,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -456,10 +463,11 @@ class MessagesFlowNode(
|
|||
focusedEventId = navTarget.focusedEventId,
|
||||
)
|
||||
val callback = object : ThreadedMessagesNode.Callback {
|
||||
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
|
||||
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
|
||||
return processEventClick(
|
||||
timelineMode = timelineMode,
|
||||
event = event,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -547,6 +555,7 @@ class MessagesFlowNode(
|
|||
private fun processEventClick(
|
||||
timelineMode: Timeline.Mode,
|
||||
event: TimelineItem.Event,
|
||||
canUseOverlay: Boolean,
|
||||
): Boolean {
|
||||
val navTarget = when (event.content) {
|
||||
is TimelineItemImageContent -> {
|
||||
|
|
@ -556,6 +565,7 @@ class MessagesFlowNode(
|
|||
content = event.content,
|
||||
mediaSource = event.content.mediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
|
|
@ -565,6 +575,7 @@ class MessagesFlowNode(
|
|||
content = event.content,
|
||||
mediaSource = event.content.mediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
is TimelineItemFileContent -> {
|
||||
|
|
@ -574,6 +585,7 @@ class MessagesFlowNode(
|
|||
content = event.content,
|
||||
mediaSource = event.content.mediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
is TimelineItemAudioContent -> {
|
||||
|
|
@ -583,6 +595,7 @@ class MessagesFlowNode(
|
|||
content = event.content,
|
||||
mediaSource = event.content.mediaSource,
|
||||
thumbnailSource = null,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
is TimelineItemLocationContent -> {
|
||||
|
|
@ -603,7 +616,11 @@ class MessagesFlowNode(
|
|||
}
|
||||
return when (navTarget) {
|
||||
is NavTarget.MediaViewer -> {
|
||||
overlay.show(navTarget)
|
||||
if (canUseOverlay) {
|
||||
overlay.show(navTarget)
|
||||
} else {
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
true
|
||||
}
|
||||
is NavTarget.LocationViewer -> {
|
||||
|
|
@ -620,6 +637,7 @@ class MessagesFlowNode(
|
|||
content: TimelineItemEventContentWithAttachment,
|
||||
mediaSource: MediaSource,
|
||||
thumbnailSource: MediaSource?,
|
||||
canUseOverlay: Boolean,
|
||||
): NavTarget {
|
||||
return NavTarget.MediaViewer(
|
||||
mode = mode,
|
||||
|
|
@ -647,6 +665,7 @@ class MessagesFlowNode(
|
|||
),
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = thumbnailSource,
|
||||
canUseOverlay = canUseOverlay,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
|||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
|
|
@ -115,7 +117,7 @@ class MessagesNode(
|
|||
)
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
|
||||
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
|
||||
fun navigateToRoomMemberDetails(userId: UserId)
|
||||
fun handlePermalinkClick(data: PermalinkData)
|
||||
|
|
@ -247,6 +249,7 @@ class MessagesNode(
|
|||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
|
|
@ -268,11 +271,11 @@ class MessagesNode(
|
|||
onRoomDetailsClick = callback::navigateToRoomDetails,
|
||||
onEventContentClick = { isLive, event ->
|
||||
if (isLive) {
|
||||
callback.handleEventClick(timelineController.mainTimelineMode(), event)
|
||||
callback.handleEventClick(timelineController.mainTimelineMode(), event, canUseOverlay)
|
||||
} else {
|
||||
val detachedTimelineMode = timelineController.detachedTimelineMode()
|
||||
if (detachedTimelineMode != null) {
|
||||
callback.handleEventClick(detachedTimelineMode, event)
|
||||
callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
|
|
@ -50,7 +52,7 @@ class PinnedMessagesListNode(
|
|||
private val permalinkParser: PermalinkParser,
|
||||
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun handleEventClick(event: TimelineItem.Event)
|
||||
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
|
||||
fun navigateToRoomMemberDetails(userId: UserId)
|
||||
fun viewInTimeline(eventId: EventId)
|
||||
fun handlePermalinkClick(data: PermalinkData.RoomLink)
|
||||
|
|
@ -103,6 +105,7 @@ class PinnedMessagesListNode(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
|
|
@ -113,7 +116,9 @@ class PinnedMessagesListNode(
|
|||
PinnedMessagesListView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
onEventClick = callback::handleEventClick,
|
||||
onEventClick = {
|
||||
callback.handleEventClick(it, canUseOverlay)
|
||||
},
|
||||
onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) },
|
||||
onLinkClick = { link -> onLinkClick(context, link.url) },
|
||||
onLinkLongClick = {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.room.alias.matches
|
|||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -124,7 +126,7 @@ class ThreadedMessagesNode(
|
|||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
|
||||
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
|
||||
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
|
||||
fun navigateToRoomMemberDetails(userId: UserId)
|
||||
fun handlePermalinkClick(data: PermalinkData)
|
||||
|
|
@ -252,6 +254,7 @@ class ThreadedMessagesNode(
|
|||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
|
|
@ -271,11 +274,11 @@ class ThreadedMessagesNode(
|
|||
onEventContentClick = { isLive, event ->
|
||||
timelineController?.let { controller ->
|
||||
if (isLive) {
|
||||
callback.handleEventClick(controller.mainTimelineMode(), event)
|
||||
callback.handleEventClick(controller.mainTimelineMode(), event, canUseOverlay)
|
||||
} else {
|
||||
val detachedTimelineMode = controller.detachedTimelineMode()
|
||||
if (detachedTimelineMode != null) {
|
||||
callback.handleEventClick(detachedTimelineMode, event)
|
||||
callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
|||
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.graphics.drawInLayer
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ import io.element.android.libraries.testtags.TestTags
|
|||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
|||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
|
|||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.focused
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -227,7 +229,11 @@ private fun ContentInitial(
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.clickable { onLearnMoreClick() }
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||
.semantics {
|
||||
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
|
||||
role = Role.Button
|
||||
},
|
||||
text = stringResource(CommonStrings.action_learn_more),
|
||||
style = ElementTheme.typography.fontBodyLgMedium
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ package io.element.android.libraries.architecture
|
|||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -88,11 +87,9 @@ inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.OverlayView(
|
|||
@Composable
|
||||
inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.BackstackWithOverlayBox(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
BackstackView()
|
||||
OverlayView()
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
|
|
@ -148,7 +150,11 @@ private fun TitleAndDescription(
|
|||
text = title,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = titleColor,
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.semantics {
|
||||
heading()
|
||||
},
|
||||
)
|
||||
if (trailingContent != null) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import androidx.compose.material3.LocalTextStyle
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
|
|
@ -48,6 +50,9 @@ fun ListSectionHeader(
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
},
|
||||
text = title,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ import androidx.compose.animation.AnimatedVisibility
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -28,7 +27,6 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -91,33 +89,31 @@ fun MediaPlayerControllerView(
|
|||
.widthIn(max = 480.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val bgColor = if (state.isPlaying) {
|
||||
ElementTheme.colors.bgCanvasDefault
|
||||
val colors = if (state.isPlaying) {
|
||||
IconButtonDefaults.iconButtonColors(
|
||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||
contentColor = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
} else {
|
||||
ElementTheme.colors.textPrimary
|
||||
IconButtonDefaults.iconButtonColors(
|
||||
containerColor = ElementTheme.colors.iconPrimary,
|
||||
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(
|
||||
color = bgColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.clickable { onTogglePlay() }
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
.size(36.dp),
|
||||
onClick = onTogglePlay,
|
||||
colors = colors,
|
||||
) {
|
||||
if (state.isPlaying) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PauseSolid(),
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
contentDescription = stringResource(CommonStrings.a11y_pause)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PlaySolid(),
|
||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||
contentDescription = stringResource(CommonStrings.a11y_play)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,52 @@ fun MediaViewerView(
|
|||
Scaffold(
|
||||
modifier,
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
AnimatedVisibility(
|
||||
visible = showOverlay,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
when (currentData) {
|
||||
is MediaViewerPageData.MediaViewerData -> {
|
||||
MediaViewerTopBar(
|
||||
data = currentData,
|
||||
canShowInfo = state.canShowInfo,
|
||||
onBackClick = onBackClick,
|
||||
onShareClick = {
|
||||
state.eventSink(MediaViewerEvent.Share(currentData))
|
||||
},
|
||||
onSaveClick = {
|
||||
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
|
||||
},
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (currentData is MediaViewerPageData.Loading) {
|
||||
Text(
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_loading_more),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = bgCanvasWithTransparency,
|
||||
),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
val pagerState = rememberPagerState(state.currentIndex, 0f) {
|
||||
|
|
@ -186,69 +232,22 @@ fun MediaViewerView(
|
|||
isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId,
|
||||
)
|
||||
// Bottom bar
|
||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
MediaViewerBottomBar(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(),
|
||||
caption = dataForPage.mediaInfo.caption,
|
||||
onHeightChange = { bottomPaddingInPixels = it },
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showOverlay,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
) {
|
||||
MediaViewerBottomBar(
|
||||
showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(),
|
||||
caption = dataForPage.mediaInfo.caption,
|
||||
onHeightChange = { bottomPaddingInPixels = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Top bar
|
||||
AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
when (currentData) {
|
||||
is MediaViewerPageData.MediaViewerData -> {
|
||||
MediaViewerTopBar(
|
||||
data = currentData,
|
||||
canShowInfo = state.canShowInfo,
|
||||
onBackClick = onBackClick,
|
||||
onShareClick = {
|
||||
state.eventSink(MediaViewerEvent.Share(currentData))
|
||||
},
|
||||
onSaveClick = {
|
||||
state.eventSink(MediaViewerEvent.SaveOnDisk(currentData))
|
||||
},
|
||||
onInfoClick = {
|
||||
state.eventSink(MediaViewerEvent.OpenInfo(currentData))
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
TopAppBar(
|
||||
title = {
|
||||
if (currentData is MediaViewerPageData.Loading) {
|
||||
Text(
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_loading_more),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = bgCanvasWithTransparency,
|
||||
),
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val bottomSheetState = state.mediaBottomSheetState) {
|
||||
|
|
@ -373,11 +372,12 @@ private fun MediaViewerPage(
|
|||
isUserSelected = isUserSelected,
|
||||
audioFocus = audioFocus,
|
||||
)
|
||||
ThumbnailView(
|
||||
mediaInfo = data.mediaInfo,
|
||||
thumbnailSource = data.thumbnailSource,
|
||||
isVisible = showThumbnail,
|
||||
)
|
||||
if (showThumbnail) {
|
||||
ThumbnailView(
|
||||
mediaInfo = data.mediaInfo,
|
||||
thumbnailSource = data.thumbnailSource,
|
||||
)
|
||||
}
|
||||
if (showError) {
|
||||
ErrorView(
|
||||
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
||||
|
|
@ -603,7 +603,6 @@ private val maxCaptionHeightLandscape = 128.dp
|
|||
@Composable
|
||||
private fun ThumbnailView(
|
||||
thumbnailSource: MediaSource?,
|
||||
isVisible: Boolean,
|
||||
mediaInfo: MediaInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -611,21 +610,19 @@ private fun ThumbnailView(
|
|||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isVisible) {
|
||||
val mediaRequestData = MediaRequestData(
|
||||
source = thumbnailSource,
|
||||
kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType)
|
||||
)
|
||||
val alpha = if (LocalInspectionMode.current) 0.1f else 1f
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(alpha),
|
||||
model = mediaRequestData,
|
||||
contentScale = ContentScale.Fit,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
val mediaRequestData = MediaRequestData(
|
||||
source = thumbnailSource,
|
||||
kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType)
|
||||
)
|
||||
val alpha = if (LocalInspectionMode.current) 0.1f else 1f
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(alpha),
|
||||
model = mediaRequestData,
|
||||
contentScale = ContentScale.Fit,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -679,7 +679,7 @@ private fun TextInputBox(
|
|||
.align(Alignment.CenterEnd),
|
||||
imageVector = CompoundIcons.InfoSolid(),
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
contentDescription = null,
|
||||
contentDescription = stringResource(CommonStrings.a11y_info),
|
||||
)
|
||||
if (showBottomSheet) {
|
||||
CaptionWarningBottomSheet(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.ui.utils.a11y
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@Composable
|
||||
fun hasExternalKeyboard(): Boolean {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
var hasExternalKeyboard by remember { mutableStateOf(activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
DisposableEffect(Unit) {
|
||||
val callback = object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
// We do not have access to onActivityConfigurationChanged, so update the value when tha Activity is resumed
|
||||
hasExternalKeyboard = activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
activity.registerActivityLifecycleCallbacks(callback)
|
||||
onDispose {
|
||||
activity.unregisterActivityLifecycleCallbacks(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasExternalKeyboard
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.ui.utils.time
|
||||
package io.element.android.libraries.ui.utils.a11y
|
||||
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2119838c9649710465dc1b8610550ce101f3016a1a6511c3f6dadc715fb75862
|
||||
size 82975
|
||||
oid sha256:33d583fac967f383a3d3535c4ac38aaccdcbf4a1d48323ff375a239dbce81838
|
||||
size 83494
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c7dea18de5eabe820fe8670cd09d7b68160a97f31e4f1c474c790d829854ef7
|
||||
size 25291
|
||||
oid sha256:b918ce7162d95c873f0029a657ee07fca2e34926e4c3cdb39eaeb10123a08721
|
||||
size 25340
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:98b39ded83aac1bb5befba6749c08a328b6855bcbd491c1a2f669c848ce72c31
|
||||
size 22982
|
||||
oid sha256:478c8ee2d55bb2a9f99b8d83c6e0eb0b316237ff1a60a6a735b5eb06f7ca083e
|
||||
size 23029
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f7aa184c28a4281a30a443b208bf3f64d9a0f85ad135ef50c999d40362c67d4
|
||||
size 24670
|
||||
oid sha256:d62cf7beeba92ee47193a871becad07f50d27e6f6fa0b85f6c620c3f1b04b6ce
|
||||
size 24697
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e13b61fcb56fbd64064bc35628957cb68507efb7fdc25280d6dbf1e6477addc
|
||||
size 22637
|
||||
oid sha256:d684c9ed8b2c38cdf7017194ca656ca8da161ab0a40ef3270c6e3988d8e7f144
|
||||
size 22664
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dbd0574dba895e157ff5b69ab23f3b389d280538810b457d2860ae5d77331705
|
||||
size 8256
|
||||
oid sha256:2b55fce1bf0d6b764aaed8eed0fbc1416c5aa1c5b6160775a641dfa10addb227
|
||||
size 8231
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e87c0990dcaf2511a2a0573a04bf14c6a9a7d24e0291fed18aa3a9ea28da572
|
||||
size 7543
|
||||
oid sha256:4ca666990cb421602e21be0dbf4ec075e8d3e94f9835464923bd31d2414d9d57
|
||||
size 7594
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ed14962b63afa972c68a53d2366b9f00906bab2f3436220bb28643fcb1d56cb
|
||||
size 7668
|
||||
oid sha256:01726f688e5149a460cb1004dff0c7700302a3fe7179829603625dd369b20e78
|
||||
size 7659
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a50093fb47b3b17a2ccd1aeb2a10fb6b5d368add8f7d458e50f0cc8643128b7c
|
||||
size 8053
|
||||
oid sha256:8c30a496415f6c00f5ce3a01c775c78994992efd68b2548ecbb5b5ffac054cc1
|
||||
size 8041
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a45336123e1eec5ea4b21c6932c1c11d6024685f736454e585a0fb6e6a85ccb2
|
||||
size 7701
|
||||
oid sha256:10f58df934f372a3b8c68c8e0c4ef61e5bfef242d65daa8f34f4999249bf9e8d
|
||||
size 7799
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:174f9f97fbce115d36fc5124d3301bd148444acdf24bdc933b5a3fb0754c883d
|
||||
size 7545
|
||||
oid sha256:6d452b48ef65df5257eb708553d667bee46b9d83b1c29a07119c54e342cf1bc2
|
||||
size 7527
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed8342b56d749db4d862aaa82d5d312089494965c2879adf18e8fd0c94f8525f
|
||||
size 13476
|
||||
oid sha256:f10ca2d461e4078e46455e007eca2d1e9c9a20dbb6bc24c681fa5164e5f50efd
|
||||
size 13531
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04a62974dd81e0786127501b8778f9d302db38a27c984d1b403884942d43accd
|
||||
size 13209
|
||||
oid sha256:da386980ce6a45e102727715b5d50f219e39dcb40a513abdfaab9fa725b8ea41
|
||||
size 13260
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:041669475888f2f0c3b2d34502ae72098547340d8c2422ea2a674d38eb6a6241
|
||||
size 207620
|
||||
oid sha256:d265f0c3fe5bc7f5a6e6d6cfa022ba16ca770b65eeb9a742ba250ff55c4b066a
|
||||
size 207625
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c56b22d79924d1f463f01428d5be1c69c8068b94a953160fae59a9f6faa112ad
|
||||
size 206047
|
||||
oid sha256:f6870b9c1a5aad4257aa4bd7b13d0be5bea281778061d71c711495aadfaafdb8
|
||||
size 206057
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e72ef42ad838837ce2427af5677931f5db68f876eccc297894c7a2cdd437cffa
|
||||
size 210729
|
||||
oid sha256:094c1de97c33431390f3ff71d73019d9af9a62ed57ba98c94e53f1f9680851fa
|
||||
size 210728
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58a323c4f06745a877ea546870fbe69ec927aea50584001b3871cad833fca1d4
|
||||
size 211343
|
||||
oid sha256:2a17d0f4bc47305b9c8a3866f70cdddb16fcb5ba10c8007c8739a329025e846a
|
||||
size 211342
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55671428c52d37c3f63f8b3410817d4e893e2f4327b31b227bc7b7d9ff84b884
|
||||
size 134680
|
||||
oid sha256:b593f6827598052cbde0e580dcd43b92fb098f6755779b95a60bb691d9ad5003
|
||||
size 134711
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7bee65567cced59131d471b5c3795347a2ef52cf64ff74b47f1c983a0b4a36f
|
||||
size 130718
|
||||
oid sha256:c0249f33aca3e50713c87a3826e71985991f0996998132c42a374c6169800023
|
||||
size 130728
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f3cea16c42488306fe29fd22362a0d37c1eeb0fbb1db48f26ad7ff18f6b196ae
|
||||
size 137605
|
||||
oid sha256:43ab4873fc5fc812bf18af50cbe620c83b273ee70305b5ea06a7aeabdf8dbc93
|
||||
size 137637
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58655e1846dbe73c287073ac909ee6c881a0ad800e8852653116fdbfd043b9a0
|
||||
size 137881
|
||||
oid sha256:33048dafab286cf0e5c84040c034d93a42d69c234edd53132d918a4a1c135ece
|
||||
size 137908
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue