ganfra
2 months ago
committed by
GitHub
29 changed files with 591 additions and 34 deletions
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.di |
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo |
||||
import dagger.Binds |
||||
import dagger.Module |
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter |
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.di.SessionScope |
||||
|
||||
@ContributesTo(SessionScope::class) |
||||
@Module |
||||
interface MessagesModule { |
||||
@Binds |
||||
fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter<PinnedMessagesBannerState> |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
sealed interface PinnedMessagesBannerEvents { |
||||
data object MoveToNextPinned : PinnedMessagesBannerEvents |
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableIntStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import javax.inject.Inject |
||||
|
||||
class PinnedMessagesBannerPresenter @Inject constructor() : Presenter<PinnedMessagesBannerState> { |
||||
@Composable |
||||
override fun present(): PinnedMessagesBannerState { |
||||
var pinnedMessageCount by remember { |
||||
mutableIntStateOf(0) |
||||
} |
||||
var currentPinnedMessageIndex by rememberSaveable { |
||||
mutableIntStateOf(0) |
||||
} |
||||
|
||||
fun handleEvent(event: PinnedMessagesBannerEvents) { |
||||
when (event) { |
||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> { |
||||
if (currentPinnedMessageIndex < pinnedMessageCount - 1) { |
||||
currentPinnedMessageIndex++ |
||||
} else { |
||||
currentPinnedMessageIndex = 0 |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return PinnedMessagesBannerState( |
||||
pinnedMessagesCount = pinnedMessageCount, |
||||
currentPinnedMessageIndex = currentPinnedMessageIndex, |
||||
eventSink = ::handleEvent |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
data class PinnedMessagesBannerState( |
||||
val pinnedMessagesCount: Int, |
||||
val currentPinnedMessageIndex: Int, |
||||
val eventSink: (PinnedMessagesBannerEvents) -> Unit |
||||
) { |
||||
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
|
||||
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> { |
||||
override val values: Sequence<PinnedMessagesBannerState> |
||||
get() = sequenceOf( |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 1, currentPinnedMessageIndex = 0), |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 2, currentPinnedMessageIndex = 0), |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 0), |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 1), |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 2), |
||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 3), |
||||
) |
||||
} |
||||
|
||||
internal fun aPinnedMessagesBannerState( |
||||
pinnedMessagesCount: Int = 0, |
||||
currentPinnedMessageIndex: Int = -1, |
||||
eventSink: (PinnedMessagesBannerEvents) -> Unit = {} |
||||
) = PinnedMessagesBannerState( |
||||
pinnedMessagesCount = pinnedMessagesCount, |
||||
currentPinnedMessageIndex = currentPinnedMessageIndex, |
||||
eventSink = eventSink |
||||
) |
@ -0,0 +1,200 @@
@@ -0,0 +1,200 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
import androidx.compose.foundation.background |
||||
import androidx.compose.foundation.clickable |
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy |
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.size |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.rememberLazyListState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.derivedStateOf |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.draw.drawBehind |
||||
import androidx.compose.ui.draw.shadow |
||||
import androidx.compose.ui.geometry.Offset |
||||
import androidx.compose.ui.graphics.Color |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.style.TextOverflow |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.compound.theme.ElementTheme |
||||
import io.element.android.compound.tokens.generated.CompoundIcons |
||||
import io.element.android.libraries.designsystem.preview.ElementPreview |
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight |
||||
import io.element.android.libraries.designsystem.theme.components.Icon |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.designsystem.theme.components.TextButton |
||||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerBorder |
||||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndicator |
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
|
||||
@Composable |
||||
fun PinnedMessagesBannerView( |
||||
state: PinnedMessagesBannerState, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder |
||||
Row( |
||||
modifier = modifier |
||||
.background(color = ElementTheme.colors.bgCanvasDefault) |
||||
.fillMaxWidth() |
||||
.drawBehind { |
||||
val strokeWidth = 0.5.dp.toPx() |
||||
val y = size.height - strokeWidth / 2 |
||||
drawLine( |
||||
borderColor, |
||||
Offset(0f, y), |
||||
Offset(size.width, y), |
||||
strokeWidth |
||||
) |
||||
drawLine( |
||||
borderColor, |
||||
Offset(0f, 0f), |
||||
Offset(size.width, 0f), |
||||
strokeWidth |
||||
) |
||||
} |
||||
.shadow(elevation = 5.dp, spotColor = Color.Transparent) |
||||
.heightIn(min = 64.dp) |
||||
.clickable { |
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) |
||||
}, |
||||
verticalAlignment = Alignment.CenterVertically, |
||||
horizontalArrangement = spacedBy(10.dp) |
||||
) { |
||||
Spacer(modifier = Modifier.width(16.dp)) |
||||
PinIndicators( |
||||
pinIndex = state.currentPinnedMessageIndex, |
||||
pinsCount = state.pinnedMessagesCount, |
||||
modifier = Modifier.heightIn(max = 40.dp) |
||||
) |
||||
Icon( |
||||
imageVector = CompoundIcons.PinSolid(), |
||||
contentDescription = null, |
||||
tint = ElementTheme.materialColors.secondary, |
||||
modifier = Modifier.size(20.dp) |
||||
) |
||||
PinnedMessageItem( |
||||
index = state.currentPinnedMessageIndex, |
||||
totalCount = state.pinnedMessagesCount, |
||||
message = "This is a pinned message", |
||||
modifier = Modifier.weight(1f) |
||||
) |
||||
TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ }) |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun PinIndicators( |
||||
pinIndex: Int, |
||||
pinsCount: Int, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val indicatorHeight by remember { |
||||
derivedStateOf { |
||||
when (pinsCount) { |
||||
0 -> 0 |
||||
1 -> 32 |
||||
2 -> 18 |
||||
else -> 11 |
||||
} |
||||
} |
||||
} |
||||
val lazyListState = rememberLazyListState() |
||||
LaunchedEffect(pinIndex) { |
||||
val viewportSize = lazyListState.layoutInfo.viewportSize |
||||
lazyListState.animateScrollToItem( |
||||
pinIndex, |
||||
indicatorHeight / 2 - viewportSize.height / 2 |
||||
) |
||||
} |
||||
LazyColumn( |
||||
modifier = modifier, |
||||
state = lazyListState, |
||||
verticalArrangement = spacedBy(2.dp), |
||||
userScrollEnabled = false |
||||
) { |
||||
items(pinsCount) { index -> |
||||
Box( |
||||
modifier = Modifier |
||||
.width(2.dp) |
||||
.height(indicatorHeight.dp) |
||||
.background( |
||||
color = if (index == pinIndex) { |
||||
ElementTheme.colors.iconAccentPrimary |
||||
} else { |
||||
ElementTheme.colors.pinnedMessageBannerIndicator |
||||
} |
||||
) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
private fun PinnedMessageItem( |
||||
index: Int, |
||||
totalCount: Int, |
||||
message: String, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount) |
||||
val fullCountMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, countMessage) |
||||
Column(modifier = modifier) { |
||||
if (totalCount > 1) { |
||||
Text( |
||||
text = annotatedTextWithBold( |
||||
text = fullCountMessage, |
||||
boldText = countMessage, |
||||
), |
||||
style = ElementTheme.typography.fontBodySmMedium, |
||||
color = ElementTheme.colors.textActionAccent, |
||||
maxLines = 1, |
||||
) |
||||
} |
||||
Text( |
||||
text = message, |
||||
style = ElementTheme.typography.fontBodyMdRegular, |
||||
color = ElementTheme.colors.textPrimary, |
||||
overflow = TextOverflow.Ellipsis, |
||||
maxLines = 1, |
||||
) |
||||
} |
||||
} |
||||
|
||||
@PreviewsDayNight |
||||
@Composable |
||||
internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { |
||||
PinnedMessagesBannerView( |
||||
state = state, |
||||
) |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.pinned.banner |
||||
|
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.tests.testutils.test |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class PinnedMessagesBannerPresenterTest { |
||||
@Test |
||||
fun `present - initial state`() = runTest { |
||||
val presenter = createPinnedMessagesBannerPresenter() |
||||
presenter.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.pinnedMessagesCount).isEqualTo(0) |
||||
assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - move to next pinned message when there is no pinned events`() = runTest { |
||||
val presenter = createPinnedMessagesBannerPresenter() |
||||
presenter.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) |
||||
// Nothing is emitted |
||||
ensureAllEventsConsumed() |
||||
} |
||||
} |
||||
|
||||
private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter { |
||||
return PinnedMessagesBannerPresenter() |
||||
} |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright (c) 2024 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 |
||||
* |
||||
* https://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.libraries.designsystem.utils |
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.derivedStateOf |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableIntStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.setValue |
||||
|
||||
/** |
||||
* Returns whether the lazy list is currently scrolling up. |
||||
*/ |
||||
@Composable |
||||
fun LazyListState.isScrollingUp(): Boolean { |
||||
var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } |
||||
var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } |
||||
return remember(this) { |
||||
derivedStateOf { |
||||
if (previousIndex != firstVisibleItemIndex) { |
||||
previousIndex > firstVisibleItemIndex |
||||
} else { |
||||
previousScrollOffset >= firstVisibleItemScrollOffset |
||||
}.also { |
||||
previousIndex = firstVisibleItemIndex |
||||
previousScrollOffset = firstVisibleItemScrollOffset |
||||
} |
||||
} |
||||
}.value |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:7114202a1de9860547c525c0dadc110ce9e2e198465218ac2c33cf65f2f0eaa2 |
||||
size 9496 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:8e6b5fd9ecc2b01cc8a83f3fe8e34352de1792a82db85c396377a18246adad1a |
||||
size 12953 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:14ca5901134299e801e204e280d731e7de4072f1d522b076eb41c5f806897ed2 |
||||
size 12905 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:bb206284c642dd665290d5d553491e622b8e15a64df7bb2dbd91ea5d3a13e19a |
||||
size 13041 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:a1940c4ee1e07c6a0198682460af1a6558fcaf14cb69ff061831cb591eb7aec3 |
||||
size 13066 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:fee5c1dbcdf7929f4762b2915584fe45b7f39916a949663f03e8d7e85e991b4b |
||||
size 12988 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:16658495b889654f152ba80a52111164f9682009b96abf1f3f20e660bd7c2407 |
||||
size 9297 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:43651c4a7f10406f1a6a4b995467ba292d4a43bc198dcf352ae2e66694154de2 |
||||
size 12340 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:cca8ebb1ec12497de7e2efc1725a2e4427eecd1d340ae8176d10f914def0af25 |
||||
size 12297 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:37bf00fbf548b7ba3d601af1ca489e07e25059c2f5a68abf9b85f4c656cf482c |
||||
size 12425 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:ed813a7003eb01a06667b10191990ed5bb3f75ee6a447cc4d52510b7e13b3724 |
||||
size 12448 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:6fa556ff7f6757c69c24e47520e08ccfd1b009d8e49a704c36d7fc4ca4186cbf |
||||
size 12378 |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:35761e520dcd7ad01d3913a98479314a1df645bdf141214e3a180fe150d2e8fd |
||||
size 59959 |
Loading…
Reference in new issue