Merge pull request #1641 from vector-im/langleyd/custom_waveform

Add custom waveform with cursor and nice gesture support.
This commit is contained in:
David Langley 2023-10-26 13:52:47 +01:00 committed by GitHub
commit b9b3bce2a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 319 additions and 174 deletions

View file

@ -65,7 +65,6 @@ dependencies {
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.zoomableimage)
implementation(libs.matrix.emojibase.bindings)
implementation(libs.audiowaveform)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View file

@ -174,6 +174,9 @@ class MediaPlayerImpl @Inject constructor(
override fun seekTo(positionMs: Long) {
player.seekTo(positionMs)
_state.update {
it.copy(currentPosition = player.currentPosition)
}
}
override fun close() {

View file

@ -44,7 +44,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider
import io.element.android.features.messages.impl.voicemessages.timeline.WaveformProgressIndicator
import io.element.android.features.messages.impl.voicemessages.timeline.Waveform
import io.element.android.features.messages.impl.voicemessages.timeline.WaveformPlaybackView
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
@ -84,13 +85,14 @@ fun TimelineItemVoiceView(
overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.width(8.dp))
WaveformProgressIndicator(
WaveformPlaybackView(
showCursor = state.button == VoiceMessageState.Button.Pause,
playbackProgress = state.progress,
waveform = Waveform(data = content.waveform),
modifier = Modifier
.height(34.dp)
.weight(1f),
progress = state.progress,
amplitudes = content.waveform,
onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) }
onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) },
)
Spacer(Modifier.width(extraPadding.getDpSize()))
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages.timeline
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlin.math.roundToInt
data class Waveform (
val data: ImmutableList<Int>
) {
companion object {
private val dataRange = 0..1024
}
fun normalisedData(maxSamplesCount: Int): ImmutableList<Float> {
if(maxSamplesCount <= 0) {
return persistentListOf()
}
// Filter the data to keep only the expected number of samples
val result = if (data.size > maxSamplesCount) {
(0..<maxSamplesCount)
.map { index ->
val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt()
data[targetIndex]
}
} else {
data
}
// Normalize the sample in the allowed range
return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList()
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages.timeline
import android.view.MotionEvent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.RequestDisallowInterceptTouchEvent
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.theme.ElementTheme
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlin.math.max
private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun WaveformPlaybackView(
playbackProgress: Float,
showCursor: Boolean,
waveform: Waveform,
modifier: Modifier = Modifier,
onSeek: (progress: Float) -> Unit = {},
brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary),
progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary),
cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary),
lineWidth: Dp = 2.dp,
linePadding: Dp = 2.dp,
minimumGraphAmplitude: Float = 2F,
) {
val seekProgress = remember { mutableStateOf<Float?>(null) }
var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) }
var canvasSizePx by remember { mutableStateOf(Size(0f, 0f)) }
val progress by remember(playbackProgress, seekProgress.value) {
derivedStateOf {
seekProgress.value ?: playbackProgress
}
}
val progressAnimated = animateFloatAsState(targetValue = progress, label = "progressAnimation")
val amplitudeDisplayCount by remember(canvasSize) {
derivedStateOf {
(canvasSize.width.value / (lineWidth.value + linePadding.value)).toInt()
}
}
val normalizedWaveformData by remember(amplitudeDisplayCount) {
derivedStateOf {
waveform.normalisedData(amplitudeDisplayCount)
}
}
val requestDisallowInterceptTouchEvent = remember { RequestDisallowInterceptTouchEvent() }
Canvas(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA)
.pointerInteropFilter(requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent) {
return@pointerInteropFilter when (it.action) {
MotionEvent.ACTION_DOWN -> {
if (it.x in 0F..canvasSizePx.width) {
requestDisallowInterceptTouchEvent.invoke(true)
seekProgress.value = it.x / canvasSizePx.width
true
} else false
}
MotionEvent.ACTION_MOVE -> {
if (it.x in 0F..canvasSizePx.width) {
seekProgress.value = it.x / canvasSizePx.width
}
true
}
MotionEvent.ACTION_UP -> {
requestDisallowInterceptTouchEvent.invoke(false)
seekProgress.value?.let(onSeek)
seekProgress.value = null
true
}
else -> false
}
}
.then(modifier)
) {
canvasSize = size.toDpSize()
canvasSizePx = size
val centerY = canvasSize.height.toPx() / 2
val cornerRadius = lineWidth / 2
normalizedWaveformData.forEachIndexed { index, amplitude ->
val drawingAmplitude = max(minimumGraphAmplitude, amplitude * (canvasSize.height.toPx() - 2))
drawRoundRect(
brush = brush,
topLeft = Offset(
x = index * (linePadding + lineWidth).toPx(),
y = centerY - drawingAmplitude / 2
),
size = Size(
width = lineWidth.toPx(),
height = drawingAmplitude
),
cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()),
style = Fill
)
}
drawRect(
brush = progressBrush,
size = Size(
width = progressAnimated.value * canvasSize.width.toPx(),
height = canvasSize.height.toPx()
),
blendMode = BlendMode.SrcAtop
)
if(showCursor || seekProgress.value != null) {
drawRoundRect(
brush = cursorBrush,
topLeft = Offset(
x = progressAnimated.value * canvasSize.width.toPx(),
y = centerY - (canvasSize.height.toPx() - 2) / 2
),
size = Size(
width = lineWidth.toPx(),
height = canvasSize.height.toPx() - 2
),
cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()),
style = Fill
)
}
}
}
@PreviewsDayNight
@Composable
internal fun WaveformPlaybackViewPreview() = ElementPreview {
Column{
WaveformPlaybackView(
modifier = Modifier.height(34.dp),
showCursor = false,
playbackProgress = 0.5f,
waveform = Waveform(persistentListOf()),
)
WaveformPlaybackView(
modifier = Modifier.height(34.dp),
showCursor = false,
playbackProgress = 0.5f,
waveform = Waveform(persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)),
)
WaveformPlaybackView(
modifier = Modifier.height(34.dp),
showCursor = true,
playbackProgress = 0.5f,
waveform = Waveform(List(1024) { it }.toPersistentList()),
)
}
}

View file

@ -1,96 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages.timeline
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.dp
import com.linc.audiowaveform.AudioWaveform
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.theme.ElementTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@Composable
fun WaveformProgressIndicator(
progress: Float,
amplitudes: ImmutableList<Int>,
modifier: Modifier = Modifier,
onSeek: (progress: Float) -> Unit = {},
) {
var seekProgress: Float? by remember { mutableStateOf(null) }
val scaledAmplitudes = remember(amplitudes) { amplitudes.scaleAmplitudes() }
AudioWaveform(
modifier = modifier,
waveformBrush = SolidColor(ElementTheme.colors.iconQuaternary),
progressBrush = SolidColor(ElementTheme.colors.iconSecondary),
onProgressChangeFinished = {
// This is to send just one onSeek callback after the user has finished seeking.
// Otherwise the AudioWaveform library would send multiple callbacks while the user is seeking.
val p = seekProgress!!
seekProgress = null
onSeek(p)
},
spikeWidth = 1.6.dp,
spikeRadius = 0.8.dp,
spikePadding = 3.dp,
progress = seekProgress ?: progress,
amplitudes = scaledAmplitudes,
onProgressChange = { seekProgress = it },
)
}
@PreviewsDayNight
@Composable
internal fun WaveformProgressIndicatorPreview() = ElementPreview {
Column {
WaveformProgressIndicator(
progress = 0.5f,
amplitudes = persistentListOf(),
)
WaveformProgressIndicator(
progress = 0.5f,
amplitudes = persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
)
WaveformProgressIndicator(
progress = 0.5f,
amplitudes = List(1024) { it }.toPersistentList()
)
}
}
/**
* Scale amplitudes to fit in the waveform view.
*
* It seems amplitudes > 128 are clipped by the waveform library.
* Workaround for https://github.com/lincollincol/compose-audiowaveform/issues/22
*
* TODO Voice messages: Remove this workaround when the waveform library is fixed.
*/
private fun ImmutableList<Int>.scaleAmplitudes(): List<Int> {
val maxAmplitude = if (isEmpty()) 1 else maxOf { it }
val scalingFactor = 128 / maxAmplitude.toFloat()
return map { (it * scalingFactor).toInt() }
}

View file

@ -166,7 +166,6 @@ maplibre = "org.maplibre.gl:android-sdk:10.2.0"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1"
opusencoder = "io.element.android:opusencoder:1.1.0"
audiowaveform = "com.github.lincollincol:compose-audiowaveform:1.1.1"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"

View file

@ -35,7 +35,6 @@ dependencyResolutionManagement {
content {
includeModule("com.github.UnifiedPush", "android-connector")
includeModule("com.github.matrix-org", "matrix-analytics-events")
includeModule("com.github.lincollincol", "compose-audiowaveform")
}
}
// To have immediate access to Rust SDK versions

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
size 6094
oid sha256:8f19f38428d2e17f2cb7a8b3de1af39cf01e48e5c854d3a64208a78c4687b867
size 6101

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
size 6044
oid sha256:6a39650b1963d637b42c36d86527124aea32d32de201ebbb6376cf351dcc7182
size 6076

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
size 6094
oid sha256:ef25bb048d7feb642509f1d5bbc646b867b89264efa32e77730650f31f0233ad
size 9895

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
size 6044
oid sha256:bc14498cb4178dbf6a103159eabe6e0c7f4458f2e10350eacd58a9c0beefff10
size 10056

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
size 5562
oid sha256:10966cc6564e45c0d65cdfee2564b7367defc1f673bf62c02291d427655d557c
size 9601

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
size 5858
oid sha256:0be1de85e0038df89677884557f9d8a35af8acba29749879469e01ddf095aca7
size 9933

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
size 6035
oid sha256:dcb3c53fff0cf52bf4ff8f8ca0bafaa7f21d1c7a6d504070de3de8f42dac2864
size 9906

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
size 5562
oid sha256:326b07336560db487616f189e3e39dc5605cabd06e15f836fd153c503ab6fa9d
size 5577

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
size 5858
oid sha256:6c70f961cc6ca573216019b3f0fabc18ae05d8bab9bb5719abca1021b9d2350b
size 6089

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
size 6035
oid sha256:0f52f22ad35d11d5124cfcc5128f97e121021dd09caa19c96f23ae8f39aa8cfe
size 6053

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
size 6094
oid sha256:18378d89af53ac54931d3feb3129ff90e0e45c707144322de3d8740fd3ce3645
size 6436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
size 6044
oid sha256:d95ae8cc103ad50397c804ba2fe16c3b4fb380456f2b3be6f99ec62877b36758
size 6429

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
size 5562
oid sha256:10058823be0d5f0a23e1712d4523188729dd7579005a48a63e8e553baea105c0
size 5936

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
size 5858
oid sha256:12eacc1babdc55ec2faed630889157ea0c9e6dca8845ecc93bc72bd645f23a9e
size 6481

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
size 6035
oid sha256:2e4ee91fc8599e4c3e1d093587a9688c36df971a94bb832e6662c86572fb08c7
size 6436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
size 6027
oid sha256:1bfffebea253932b08a308ac098a392910233e4a8524a898b459842849854c91
size 6036

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
size 5992
oid sha256:b846d71d9d0019f55ee34e4ef38375f1501663a855f2c05742239a08d5b7501d
size 6020

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
size 6027
oid sha256:ca13f10ca5fd6194829c3cef02fa5cd0c00790f23c82d2cf70ac5d1d58babb54
size 9656

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
size 5992
oid sha256:1dfdaca0c643c81a281658b4cee4d0e938d542a57964c3831496d04db543da45
size 9827

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
size 5549
oid sha256:ad2ccde01384563471412843a48180b7acb3ba9bbedb9af60f96d403e564073d
size 9566

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
size 5807
oid sha256:e32db0169cf50c2120a16efea3cb2b0f8a72f81ddce7a303c13bc2bf38c487ff
size 9653

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
size 5957
oid sha256:796854c3af1c45268c810f393842287d5fb3424f267f987f47bcd105e2a88448
size 9621

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
size 5549
oid sha256:a0a6b88a5d7f001309dcb03da6151ac2949705b32a19301660a24fe02dc6fcdd
size 5562

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
size 5807
oid sha256:2f8562e1e599e16ecc05ec0471de70b9de9a88b347f622bc723f21cb5379b102
size 6033

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
size 5957
oid sha256:32eba449e245eed3bc67c1ffdf47e267a6de2bf6832739f2902949af297391aa
size 6001

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
size 6027
oid sha256:a2649d3974b096f17a6e80d728b08409c0f1b28ed649f33c7798f6223f55ca29
size 6343

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
size 5992
oid sha256:7841691e0bbcfa0655304ac6a7c0ea24353aae9bfc264dc7978c2a4526781ee3
size 6357

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
size 5549
oid sha256:d1a1ad6a3c02f24a8b811197f6999d93db57d3c1e9b867638c88f4759e9f1b87
size 5926

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
size 5807
oid sha256:0adbfdd9131913c11d7e4777b4bdfbda84b3a9c6f1784781698d9208023e9c39
size 6400

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
size 5957
oid sha256:cf18143a576ef0a136ca6e07c51521f5d41b6b5f381d168ff311cdf099752550
size 6349

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f085bc69b2ac416194526e5e6e1f73eb9b0f83acb3b775c80d3a9f88fe6e2eeb
size 22953
oid sha256:216e36042ad1ef11331d8610ab32383112bde38e314ae0d8c42d23931c7874a2
size 43748

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:acfc96b5c860be6173b38443c7e59c7312c2e6625626855c0efb8552b4bd92f2
size 22263
oid sha256:2542c95c067d4d65e76ea606801dc3dbfc196d8128a8547ed3e1bd93b84d4ab9
size 42513

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:efc60fcfb39718bfce62f16e82a0c85249b71bf850b2d996bb1234a882261bc9
size 10021

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6f5b1b3ce84e262eec2f5fea04e4d8d15d0ec4dabd589b73b8b0c0b41afb514
size 9754

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
size 4457

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
size 4464