Merge pull request #920 from vector-im/feature/bma/slidingSyncState
Sliding sync state rendering
This commit is contained in:
commit
c882cab67f
29 changed files with 200 additions and 32 deletions
|
|
@ -198,7 +198,6 @@ dependencies {
|
||||||
allLibrariesImpl()
|
allLibrariesImpl()
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir, logger)
|
allFeaturesImpl(rootDir, logger)
|
||||||
implementation(projects.tests.uitests)
|
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ dependencies {
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.permissions.noop)
|
implementation(projects.libraries.permissions.noop)
|
||||||
|
|
||||||
implementation(projects.tests.uitests)
|
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
|
||||||
implementation(projects.features.ftue.api)
|
implementation(projects.features.ftue.api)
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
syncService.syncState,
|
syncService.syncState,
|
||||||
networkMonitor.connectivity
|
networkMonitor.connectivity
|
||||||
) { syncState, networkStatus ->
|
) { syncState, networkStatus ->
|
||||||
syncState == SyncState.InError && networkStatus == NetworkStatus.Online
|
syncState == SyncState.Error && networkStatus == NetworkStatus.Online
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { restartSync ->
|
.collect { restartSync ->
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
|
|
@ -52,6 +53,7 @@ class LoggedInPresenter @Inject constructor(
|
||||||
pushService.registerWith(matrixClient, pushProvider, distributor)
|
pushService.registerWith(matrixClient, pushProvider, distributor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val syncState = matrixClient.syncService().syncState.collectAsState()
|
||||||
val permissionsState = postNotificationPermissionsPresenter.present()
|
val permissionsState = postNotificationPermissionsPresenter.present()
|
||||||
|
|
||||||
// fun handleEvents(event: LoggedInEvents) {
|
// fun handleEvents(event: LoggedInEvents) {
|
||||||
|
|
@ -60,6 +62,7 @@ class LoggedInPresenter @Inject constructor(
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return LoggedInState(
|
return LoggedInState(
|
||||||
|
syncState = syncState.value,
|
||||||
permissionsState = permissionsState,
|
permissionsState = permissionsState,
|
||||||
// eventSink = ::handleEvents
|
// eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.permissions.api.PermissionsState
|
import io.element.android.libraries.permissions.api.PermissionsState
|
||||||
|
|
||||||
data class LoggedInState(
|
data class LoggedInState(
|
||||||
|
val syncState: SyncState,
|
||||||
val permissionsState: PermissionsState,
|
val permissionsState: PermissionsState,
|
||||||
// val eventSink: (LoggedInEvents) -> Unit
|
// val eventSink: (LoggedInEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,22 @@
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
|
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
|
||||||
|
|
||||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
||||||
override val values: Sequence<LoggedInState>
|
override val values: Sequence<LoggedInState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aLoggedInState(),
|
aLoggedInState(),
|
||||||
|
aLoggedInState(syncState = SyncState.Idle),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aLoggedInState() = LoggedInState(
|
fun aLoggedInState(
|
||||||
|
syncState: SyncState = SyncState.Running,
|
||||||
|
) = LoggedInState(
|
||||||
|
syncState = syncState,
|
||||||
permissionsState = createDummyPostNotificationPermissionsState(),
|
permissionsState = createDummyPostNotificationPermissionsState(),
|
||||||
// eventSink = {}
|
// eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
import io.element.android.libraries.androidutils.system.openAppSettingsPage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.permissions.api.PermissionsView
|
import io.element.android.libraries.permissions.api.PermissionsView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -33,25 +38,27 @@ fun LoggedInView(
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
PermissionsView(
|
Box(
|
||||||
state = state.permissionsState,
|
modifier = modifier
|
||||||
modifier = modifier,
|
.fillMaxSize()
|
||||||
openSystemSettings = context::openAppSettingsPage
|
.systemBarsPadding()
|
||||||
)
|
) {
|
||||||
|
SyncStateView(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
syncState = state.syncState,
|
||||||
|
)
|
||||||
|
PermissionsView(
|
||||||
|
state = state.permissionsState,
|
||||||
|
openSystemSettings = context::openAppSettingsPage
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun LoggedInViewLightPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun LoggedInViewDarkPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) =
|
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentToPreview(state: LoggedInState) {
|
|
||||||
LoggedInView(
|
LoggedInView(
|
||||||
state = state
|
state = state
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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.appnav.loggedin
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.progressSemantics
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SyncStateView(
|
||||||
|
syncState: SyncState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val animationSpec = spring<Float>(stiffness = 500F)
|
||||||
|
AnimatedVisibility(
|
||||||
|
modifier = modifier,
|
||||||
|
visible = syncState.mustBeVisible(),
|
||||||
|
enter = fadeIn(animationSpec = animationSpec),
|
||||||
|
exit = fadeOut(animationSpec = animationSpec),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
shadowElevation = 8.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = ElementTheme.colors.bgSubtleSecondary)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.progressSemantics()
|
||||||
|
.size(12.dp),
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
strokeWidth = 1.5.dp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.common_syncing),
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SyncState.mustBeVisible() = when (this) {
|
||||||
|
SyncState.Idle -> true /* Cold start of the app */
|
||||||
|
SyncState.Running -> false
|
||||||
|
SyncState.Error -> false /* In this case, the network error banner can be displayed */
|
||||||
|
SyncState.Terminated -> true /* The app is resumed and the sync is started again */
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
fun SyncStateViewPreview() = ElementPreview {
|
||||||
|
// Add a box to see the shadow
|
||||||
|
Box(modifier = Modifier.padding(24.dp)) {
|
||||||
|
SyncStateView(
|
||||||
|
syncState = SyncState.Idle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.libraries.matrix.api.sync
|
||||||
|
|
||||||
enum class SyncState {
|
enum class SyncState {
|
||||||
Idle,
|
Idle,
|
||||||
Syncing,
|
Running,
|
||||||
InError,
|
Error,
|
||||||
Terminated,
|
Terminated,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ class RustMatrixClient constructor(
|
||||||
client.setDelegate(clientDelegate)
|
client.setDelegate(clientDelegate)
|
||||||
rustSyncService.syncState
|
rustSyncService.syncState
|
||||||
.onEach { syncState ->
|
.onEach { syncState ->
|
||||||
if (syncState == SyncState.Syncing) {
|
if (syncState == SyncState.Running) {
|
||||||
onSlidingSyncUpdate()
|
onSlidingSyncUpdate()
|
||||||
}
|
}
|
||||||
}.launchIn(sessionCoroutineScope)
|
}.launchIn(sessionCoroutineScope)
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ internal fun RoomListServiceState.toSyncState(): SyncState {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
RoomListServiceState.INIT,
|
RoomListServiceState.INIT,
|
||||||
RoomListServiceState.SETTING_UP -> SyncState.Idle
|
RoomListServiceState.SETTING_UP -> SyncState.Idle
|
||||||
RoomListServiceState.RUNNING -> SyncState.Syncing
|
RoomListServiceState.RUNNING -> SyncState.Running
|
||||||
RoomListServiceState.ERROR -> SyncState.InError
|
RoomListServiceState.ERROR -> SyncState.Error
|
||||||
RoomListServiceState.TERMINATED -> SyncState.Terminated
|
RoomListServiceState.TERMINATED -> SyncState.Terminated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,8 +33,8 @@ internal fun RoomListServiceState.toSyncState(): SyncState {
|
||||||
internal fun SyncServiceState.toSyncState(): SyncState {
|
internal fun SyncServiceState.toSyncState(): SyncState {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
SyncServiceState.IDLE -> SyncState.Idle
|
SyncServiceState.IDLE -> SyncState.Idle
|
||||||
SyncServiceState.RUNNING -> SyncState.Syncing
|
SyncServiceState.RUNNING -> SyncState.Running
|
||||||
SyncServiceState.TERMINATED -> SyncState.Terminated
|
SyncServiceState.TERMINATED -> SyncState.Terminated
|
||||||
SyncServiceState.ERROR -> SyncState.InError
|
SyncServiceState.ERROR -> SyncState.Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ class FakeSyncService : SyncService {
|
||||||
private val syncStateFlow = MutableStateFlow(SyncState.Idle)
|
private val syncStateFlow = MutableStateFlow(SyncState.Idle)
|
||||||
|
|
||||||
fun simulateError() {
|
fun simulateError() {
|
||||||
syncStateFlow.value = SyncState.InError
|
syncStateFlow.value = SyncState.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startSync(): Result<Unit> {
|
override suspend fun startSync(): Result<Unit> {
|
||||||
syncStateFlow.value = SyncState.Syncing
|
syncStateFlow.value = SyncState.Running
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.showkase)
|
implementation(libs.showkase)
|
||||||
|
|
||||||
|
// TODO There is a Resources.NotFoundException maybe due to the mipmap, even if we have
|
||||||
|
// `testOptions { unitTests.isIncludeAndroidResources = true }` in the app build.gradle.kts file
|
||||||
|
// implementation(projects.app)
|
||||||
|
implementation(projects.appnav)
|
||||||
allLibrariesImpl()
|
allLibrariesImpl()
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir, logger)
|
allFeaturesImpl(rootDir, logger)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
|
||||||
|
size 4457
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ee84ea7218ad52104a3f030d3daefa5174e122c511a8d7ab7515fb50e9eb8e01
|
||||||
|
size 9580
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753
|
||||||
|
size 4464
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e2d3f6f72be52a27e0ce15f091a44d7796f91a10eb7dab54787631b2a6d33f74
|
||||||
|
size 8220
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:90b1daf3ab9e6377fa703ea1e13943eaf40e84ccbc61212d606a527759ff20a9
|
||||||
|
size 9689
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:58a097099974131832bae557acf824154bb76fb34d309cb68bd2ae5e658d5371
|
||||||
|
size 8347
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:da4188dc606f0735fd4093acad34897c866d8c4d20b3e0ec0618685f7302cbf5
|
||||||
|
size 9288
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e27b1ea7bfa4eb97af3c2d433fbe3f5ca21458e4458d91b39c9c2bb3d7b02abc
|
||||||
|
size 11306
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2bbeb21faf320226f42aad179b955952ad8e4b5d5645c45bfbbc02166baf0662
|
||||||
|
size 9641
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:499e17b52d57f8b0d8984e32457b3922c0647be5b835c02a01ed927fe004ec4d
|
||||||
|
size 11610
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c51726746993ad3ed0f6da9fe49a02b87ec518495e04f6cdb28ab4454dc72938
|
||||||
|
size 25457
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:14ffd433679cbadf1bb325257786a834fce3e362d2ed763c1823b0c958c0f38a
|
||||||
|
size 27496
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:85d3ea85f270fae4db7e14ab9c2c19a6c4f3a68e1c2112010471cb69cdb71b2b
|
||||||
|
size 22071
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fd79483dea8ce19284069511ec96dfc68704a9ff2f1a33be312bbcdb387123c6
|
||||||
|
size 26655
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:931423fea194fb1bede5cb3d73083fb65335716ca088fbda707f4f4727eae4dd
|
||||||
|
size 28757
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bfa6e3d8698a2327d6e3cc14318856f221355581e6c661b92d2e215daf8f30cd
|
||||||
|
size 22879
|
||||||
Loading…
Add table
Add a link
Reference in a new issue