Location expanded view: show own location (#916)
If the location permission is granted: - Shows the user's own location - Shows a button to center the map on it Part of: - https://github.com/vector-im/element-meta/issues/1678
This commit is contained in:
parent
b2436a9d7c
commit
c135da2562
16 changed files with 188 additions and 38 deletions
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.location.impl
|
package io.element.android.features.location.impl
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
|
@ -60,4 +61,6 @@ object MapDefaults {
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
const val DEFAULT_ZOOM = 15.0
|
const val DEFAULT_ZOOM = 15.0
|
||||||
|
|
||||||
|
val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.location.impl.send
|
package io.element.android.features.location.impl.send
|
||||||
|
|
||||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
|
||||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
|
@ -56,9 +54,7 @@ class SendLocationPresenter @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<SendLocationState> {
|
) : Presenter<SendLocationState> {
|
||||||
|
|
||||||
private val permissionsPresenter = permissionsPresenterFactory.create(
|
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
|
||||||
listOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): SendLocationState {
|
override fun present(): SendLocationState {
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
sealed interface ShowLocationEvents {
|
sealed interface ShowLocationEvents {
|
||||||
object Share : ShowLocationEvents
|
object Share : ShowLocationEvents
|
||||||
|
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,21 @@
|
||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
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 dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.features.location.impl.MapDefaults
|
||||||
|
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||||
|
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
class ShowLocationPresenter @AssistedInject constructor(
|
class ShowLocationPresenter @AssistedInject constructor(
|
||||||
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
private val actions: LocationActions,
|
private val actions: LocationActions,
|
||||||
@Assisted private val location: Location,
|
@Assisted private val location: Location,
|
||||||
@Assisted private val description: String?
|
@Assisted private val description: String?
|
||||||
|
|
@ -34,15 +42,26 @@ class ShowLocationPresenter @AssistedInject constructor(
|
||||||
fun create(location: Location, description: String?): ShowLocationPresenter
|
fun create(location: Location, description: String?): ShowLocationPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ShowLocationState {
|
override fun present(): ShowLocationState {
|
||||||
|
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||||
|
var isTrackMyLocation by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun handleEvents(event: ShowLocationEvents) {
|
||||||
|
when (event) {
|
||||||
|
ShowLocationEvents.Share -> actions.share(location, description)
|
||||||
|
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ShowLocationState(
|
return ShowLocationState(
|
||||||
location = location,
|
location = location,
|
||||||
description = description
|
description = description,
|
||||||
) {
|
hasLocationPermission = permissionsState.isAnyGranted,
|
||||||
when (it) {
|
isTrackMyLocation = isTrackMyLocation,
|
||||||
ShowLocationEvents.Share -> actions.share(location, description)
|
eventSink = ::handleEvents,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,7 @@ import io.element.android.features.location.api.Location
|
||||||
data class ShowLocationState(
|
data class ShowLocationState(
|
||||||
val location: Location,
|
val location: Location,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val hasLocationPermission: Boolean,
|
||||||
|
val isTrackMyLocation: Boolean,
|
||||||
val eventSink: (ShowLocationEvents) -> Unit,
|
val eventSink: (ShowLocationEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,37 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = null,
|
description = null,
|
||||||
|
hasLocationPermission = false,
|
||||||
|
isTrackMyLocation = false,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = null,
|
||||||
|
hasLocationPermission = true,
|
||||||
|
isTrackMyLocation = false,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
|
ShowLocationState(
|
||||||
|
Location(1.23, 2.34, 4f),
|
||||||
|
description = null,
|
||||||
|
hasLocationPermission = true,
|
||||||
|
isTrackMyLocation = true,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "My favourite place!",
|
description = "My favourite place!",
|
||||||
|
hasLocationPermission = false,
|
||||||
|
isTrackMyLocation = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
ShowLocationState(
|
ShowLocationState(
|
||||||
Location(1.23, 2.34, 4f),
|
Location(1.23, 2.34, 4f),
|
||||||
description = "For some reason I decided to write a small essay in the location description. " +
|
description = "For some reason I decided to write a small essay in the location description. " +
|
||||||
"It is so long that it will wrap onto more than two lines!",
|
"It is so long that it will wrap onto more than two lines!",
|
||||||
|
hasLocationPermission = false,
|
||||||
|
isTrackMyLocation = false,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocationSearching
|
||||||
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
import androidx.compose.material.icons.outlined.Share
|
import androidx.compose.material.icons.outlined.Share
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -37,15 +40,19 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||||
import io.element.android.features.location.impl.MapDefaults
|
import io.element.android.features.location.impl.MapDefaults
|
||||||
|
import io.element.android.features.location.impl.send.SendLocationState
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.maplibre.compose.CameraMode
|
||||||
|
import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason
|
||||||
import io.element.android.libraries.maplibre.compose.IconAnchor
|
import io.element.android.libraries.maplibre.compose.IconAnchor
|
||||||
import io.element.android.libraries.maplibre.compose.MapboxMap
|
import io.element.android.libraries.maplibre.compose.MapboxMap
|
||||||
import io.element.android.libraries.maplibre.compose.Symbol
|
import io.element.android.libraries.maplibre.compose.Symbol
|
||||||
|
|
@ -64,7 +71,33 @@ fun ShowLocationView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Scaffold(modifier,
|
val cameraPositionState = rememberCameraPositionState {
|
||||||
|
position = CameraPosition.Builder()
|
||||||
|
.target(LatLng(state.location.lat, state.location.lon))
|
||||||
|
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.isTrackMyLocation) {
|
||||||
|
when (state.isTrackMyLocation) {
|
||||||
|
false -> cameraPositionState.cameraMode = CameraMode.NONE
|
||||||
|
true -> {
|
||||||
|
cameraPositionState.position = CameraPosition.Builder()
|
||||||
|
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||||
|
.build()
|
||||||
|
cameraPositionState.cameraMode = CameraMode.TRACKING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(cameraPositionState.isMoving) {
|
||||||
|
if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
|
||||||
|
state.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
|
|
@ -82,7 +115,19 @@ fun ShowLocationView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (state.hasLocationPermission) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||||
|
) {
|
||||||
|
when (state.isTrackMyLocation) {
|
||||||
|
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
||||||
|
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -107,14 +152,12 @@ fun ShowLocationView(
|
||||||
styleUri = rememberTileStyleUrl(),
|
styleUri = rememberTileStyleUrl(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
images = mapOf(PIN_ID to DesignSystemR.drawable.pin).toImmutableMap(),
|
images = mapOf(PIN_ID to DesignSystemR.drawable.pin).toImmutableMap(),
|
||||||
cameraPositionState = rememberCameraPositionState {
|
cameraPositionState = cameraPositionState,
|
||||||
position = CameraPosition.Builder()
|
|
||||||
.target(LatLng(state.location.lat, state.location.lon))
|
|
||||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
|
||||||
.build()
|
|
||||||
},
|
|
||||||
uiSettings = MapDefaults.uiSettings,
|
uiSettings = MapDefaults.uiSettings,
|
||||||
symbolManagerSettings = MapDefaults.symbolManagerSettings,
|
symbolManagerSettings = MapDefaults.symbolManagerSettings,
|
||||||
|
locationSettings = MapDefaults.locationSettings.copy(
|
||||||
|
locationEnabled = state.hasLocationPermission,
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Symbol(
|
Symbol(
|
||||||
iconId = PIN_ID,
|
iconId = PIN_ID,
|
||||||
|
|
|
||||||
|
|
@ -21,39 +21,72 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
|
import io.element.android.features.location.impl.permissions.PermissionsPresenter
|
||||||
|
import io.element.android.features.location.impl.permissions.PermissionsPresenterFake
|
||||||
|
import io.element.android.features.location.impl.permissions.PermissionsState
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ShowLocationPresenterTest {
|
class ShowLocationPresenterTest {
|
||||||
|
|
||||||
|
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||||
private val actions = FakeLocationActions()
|
private val actions = FakeLocationActions()
|
||||||
private val location = Location(1.23, 4.56, 7.8f)
|
private val location = Location(1.23, 4.56, 7.8f)
|
||||||
|
private val presenter = ShowLocationPresenter(
|
||||||
@Test
|
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
||||||
fun `emits initial state`() = runTest {
|
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
|
||||||
val presenter = ShowLocationPresenter(
|
},
|
||||||
actions,
|
actions,
|
||||||
location,
|
location,
|
||||||
A_DESCRIPTION,
|
A_DESCRIPTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits initial state with no location permission`() = runTest {
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||||
|
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||||
|
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(false)
|
||||||
|
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits initial state with location permission`() = runTest {
|
||||||
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.location).isEqualTo(location)
|
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||||
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||||
|
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true)
|
||||||
|
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits initial state with partial location permission`() = runTest {
|
||||||
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||||
|
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||||
|
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true)
|
||||||
|
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `uses action to share location`() = runTest {
|
fun `uses action to share location`() = runTest {
|
||||||
val presenter = ShowLocationPresenter(
|
|
||||||
actions,
|
|
||||||
location,
|
|
||||||
A_DESCRIPTION,
|
|
||||||
)
|
|
||||||
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -65,6 +98,27 @@ class ShowLocationPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `centers on user location`() = runTest {
|
||||||
|
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true)
|
||||||
|
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||||
|
|
||||||
|
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||||
|
val trackMyLocationState = awaitItem()
|
||||||
|
|
||||||
|
delay(1)
|
||||||
|
|
||||||
|
Truth.assertThat(trackMyLocationState.hasLocationPermission).isEqualTo(true)
|
||||||
|
Truth.assertThat(trackMyLocationState.isTrackMyLocation).isEqualTo(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val A_DESCRIPTION = "My happy place"
|
private const val A_DESCRIPTION = "My happy place"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f19878925f3b5b377a91885540fb15d29a5b78d8be2282d64e8809af0bbf5ff4
|
oid sha256:8d0a5de3c4e09d76b6453ccc6ace5c690d540d945a62e630474932734209058a
|
||||||
size 12195
|
size 11716
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c936d2d804bc9e98fcc49430f11ddaa572b05fc8d3a0df93ad6521ee8e78f708
|
oid sha256:9218d1d514342c400051bebb10ef458c99103fda618bba45e61a524c5a58eb63
|
||||||
size 21806
|
size 11903
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f19878925f3b5b377a91885540fb15d29a5b78d8be2282d64e8809af0bbf5ff4
|
||||||
|
size 12195
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c936d2d804bc9e98fcc49430f11ddaa572b05fc8d3a0df93ad6521ee8e78f708
|
||||||
|
size 21806
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:065d2a09680e35540b870862ec0ba5c54182e016017938ef64e510b6132333c9
|
oid sha256:e39c5ba30983b034886a6adc330d1510b3a48c40511697edcaf6530716c7ba2e
|
||||||
size 13389
|
size 12500
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0bb88c64bc68b10b1fb709135f445de5a5e4d78623448d0fef97504e025d5f6d
|
oid sha256:e4ff75e74c19280308878e3001a8791aaa735439cc667946fefd21070611630e
|
||||||
size 24551
|
size 12698
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:065d2a09680e35540b870862ec0ba5c54182e016017938ef64e510b6132333c9
|
||||||
|
size 13389
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0bb88c64bc68b10b1fb709135f445de5a5e4d78623448d0fef97504e025d5f6d
|
||||||
|
size 24551
|
||||||
Loading…
Add table
Add a link
Reference in a new issue