Merge pull request #1641 from vector-im/langleyd/custom_waveform
Add custom waveform with cursor and nice gesture support.
This commit is contained in:
commit
b9b3bce2a2
44 changed files with 319 additions and 174 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
|
||||
size 6094
|
||||
oid sha256:8f19f38428d2e17f2cb7a8b3de1af39cf01e48e5c854d3a64208a78c4687b867
|
||||
size 6101
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
|
||||
size 6044
|
||||
oid sha256:6a39650b1963d637b42c36d86527124aea32d32de201ebbb6376cf351dcc7182
|
||||
size 6076
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
|
||||
size 6094
|
||||
oid sha256:ef25bb048d7feb642509f1d5bbc646b867b89264efa32e77730650f31f0233ad
|
||||
size 9895
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
|
||||
size 6044
|
||||
oid sha256:bc14498cb4178dbf6a103159eabe6e0c7f4458f2e10350eacd58a9c0beefff10
|
||||
size 10056
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
|
||||
size 5562
|
||||
oid sha256:10966cc6564e45c0d65cdfee2564b7367defc1f673bf62c02291d427655d557c
|
||||
size 9601
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
|
||||
size 5858
|
||||
oid sha256:0be1de85e0038df89677884557f9d8a35af8acba29749879469e01ddf095aca7
|
||||
size 9933
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
|
||||
size 6035
|
||||
oid sha256:dcb3c53fff0cf52bf4ff8f8ca0bafaa7f21d1c7a6d504070de3de8f42dac2864
|
||||
size 9906
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
|
||||
size 5562
|
||||
oid sha256:326b07336560db487616f189e3e39dc5605cabd06e15f836fd153c503ab6fa9d
|
||||
size 5577
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
|
||||
size 5858
|
||||
oid sha256:6c70f961cc6ca573216019b3f0fabc18ae05d8bab9bb5719abca1021b9d2350b
|
||||
size 6089
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
|
||||
size 6035
|
||||
oid sha256:0f52f22ad35d11d5124cfcc5128f97e121021dd09caa19c96f23ae8f39aa8cfe
|
||||
size 6053
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357
|
||||
size 6094
|
||||
oid sha256:18378d89af53ac54931d3feb3129ff90e0e45c707144322de3d8740fd3ce3645
|
||||
size 6436
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd
|
||||
size 6044
|
||||
oid sha256:d95ae8cc103ad50397c804ba2fe16c3b4fb380456f2b3be6f99ec62877b36758
|
||||
size 6429
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e
|
||||
size 5562
|
||||
oid sha256:10058823be0d5f0a23e1712d4523188729dd7579005a48a63e8e553baea105c0
|
||||
size 5936
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8
|
||||
size 5858
|
||||
oid sha256:12eacc1babdc55ec2faed630889157ea0c9e6dca8845ecc93bc72bd645f23a9e
|
||||
size 6481
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1
|
||||
size 6035
|
||||
oid sha256:2e4ee91fc8599e4c3e1d093587a9688c36df971a94bb832e6662c86572fb08c7
|
||||
size 6436
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
|
||||
size 6027
|
||||
oid sha256:1bfffebea253932b08a308ac098a392910233e4a8524a898b459842849854c91
|
||||
size 6036
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
|
||||
size 5992
|
||||
oid sha256:b846d71d9d0019f55ee34e4ef38375f1501663a855f2c05742239a08d5b7501d
|
||||
size 6020
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
|
||||
size 6027
|
||||
oid sha256:ca13f10ca5fd6194829c3cef02fa5cd0c00790f23c82d2cf70ac5d1d58babb54
|
||||
size 9656
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
|
||||
size 5992
|
||||
oid sha256:1dfdaca0c643c81a281658b4cee4d0e938d542a57964c3831496d04db543da45
|
||||
size 9827
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
|
||||
size 5549
|
||||
oid sha256:ad2ccde01384563471412843a48180b7acb3ba9bbedb9af60f96d403e564073d
|
||||
size 9566
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
|
||||
size 5807
|
||||
oid sha256:e32db0169cf50c2120a16efea3cb2b0f8a72f81ddce7a303c13bc2bf38c487ff
|
||||
size 9653
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
|
||||
size 5957
|
||||
oid sha256:796854c3af1c45268c810f393842287d5fb3424f267f987f47bcd105e2a88448
|
||||
size 9621
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
|
||||
size 5549
|
||||
oid sha256:a0a6b88a5d7f001309dcb03da6151ac2949705b32a19301660a24fe02dc6fcdd
|
||||
size 5562
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
|
||||
size 5807
|
||||
oid sha256:2f8562e1e599e16ecc05ec0471de70b9de9a88b347f622bc723f21cb5379b102
|
||||
size 6033
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
|
||||
size 5957
|
||||
oid sha256:32eba449e245eed3bc67c1ffdf47e267a6de2bf6832739f2902949af297391aa
|
||||
size 6001
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031
|
||||
size 6027
|
||||
oid sha256:a2649d3974b096f17a6e80d728b08409c0f1b28ed649f33c7798f6223f55ca29
|
||||
size 6343
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290
|
||||
size 5992
|
||||
oid sha256:7841691e0bbcfa0655304ac6a7c0ea24353aae9bfc264dc7978c2a4526781ee3
|
||||
size 6357
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2
|
||||
size 5549
|
||||
oid sha256:d1a1ad6a3c02f24a8b811197f6999d93db57d3c1e9b867638c88f4759e9f1b87
|
||||
size 5926
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426
|
||||
size 5807
|
||||
oid sha256:0adbfdd9131913c11d7e4777b4bdfbda84b3a9c6f1784781698d9208023e9c39
|
||||
size 6400
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d
|
||||
size 5957
|
||||
oid sha256:cf18143a576ef0a136ca6e07c51521f5d41b6b5f381d168ff311cdf099752550
|
||||
size 6349
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f085bc69b2ac416194526e5e6e1f73eb9b0f83acb3b775c80d3a9f88fe6e2eeb
|
||||
size 22953
|
||||
oid sha256:216e36042ad1ef11331d8610ab32383112bde38e314ae0d8c42d23931c7874a2
|
||||
size 43748
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:acfc96b5c860be6173b38443c7e59c7312c2e6625626855c0efb8552b4bd92f2
|
||||
size 22263
|
||||
oid sha256:2542c95c067d4d65e76ea606801dc3dbfc196d8128a8547ed3e1bd93b84d4ab9
|
||||
size 42513
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:efc60fcfb39718bfce62f16e82a0c85249b71bf850b2d996bb1234a882261bc9
|
||||
size 10021
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6f5b1b3ce84e262eec2f5fea04e4d8d15d0ec4dabd589b73b8b0c0b41afb514
|
||||
size 9754
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||
size 4457
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||
size 4464
|
||||
Loading…
Add table
Add a link
Reference in a new issue