Skip to content
Snippets Groups Projects
Verified Commit 3be9694f authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: build initial demo

parent 9945a4f1
Branches
No related tags found
No related merge requests found
Showing
with 722 additions and 0 deletions
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavController
import de.justjanne.libquassel.protocol.models.ids.BufferId
import de.justjanne.libquassel.protocol.syncables.state.BufferViewConfigState
import de.justjanne.libquassel.protocol.util.combineLatest
import de.justjanne.libquassel.protocol.util.flatMap
import de.justjanne.quasseldroid.service.QuasselBackend
import de.justjanne.quasseldroid.util.mapNullable
import de.justjanne.quasseldroid.util.rememberFlow
@Composable
fun HomeView(backend: QuasselBackend, navController: NavController) {
val session = rememberFlow(null) { backend.flow() }
val bufferViewConfigs: List<BufferViewConfigState> = rememberFlow(emptyList()) {
backend.flow()
.flatMap()
.mapNullable { it.bufferViewManager }
.flatMap()
.mapNullable { it.bufferViewConfigs() }
.combineLatest()
}
val initStatus = rememberFlow(null) {
backend.flow()
.mapNullable { it.baseInitHandler }
.flatMap()
}
val context = LocalContext.current
Column {
Text("Side: ${session?.side}")
if (initStatus != null) {
val done = initStatus.total - initStatus.waiting.size
Text("Init: ${initStatus.started} $done/ ${initStatus.total}")
}
Button(onClick = { navController.navigate("coreInfo") }) {
Text("Core Info")
}
Button(onClick = {
backend.disconnect(context)
navController.navigate("login")
}) {
Text("Disconnect")
}
Text("BufferViewConfigs: ${bufferViewConfigs.size}")
LazyColumn {
items(bufferViewConfigs, key = BufferViewConfigState::bufferViewId) {
Row {
Text("${it.bufferViewId}: ${it.bufferViewName}")
}
}
}
}
}
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import de.justjanne.quasseldroid.service.ConnectionData
import de.justjanne.quasseldroid.service.QuasselBackend
import de.justjanne.quasseldroid.util.TextFieldValueSaver
import java.net.InetSocketAddress
@Composable
fun LoginRoute(backend: QuasselBackend, navController: NavController) {
val context = LocalContext.current
LoginView(onLogin = {
if (backend.login(context, it)) {
navController.navigate("home")
}
})
}
@Preview(name = "Login", showBackground = true)
@Composable
fun LoginView(onLogin: (ConnectionData) -> Unit = {}) {
val (host, setHost) = rememberSaveable(stateSaver = TextFieldValueSaver) {
mutableStateOf(TextFieldValue())
}
val (port, setPort) = rememberSaveable(stateSaver = TextFieldValueSaver) {
mutableStateOf(TextFieldValue("4242"))
}
val (username, setUsername) = rememberSaveable(stateSaver = TextFieldValueSaver) {
mutableStateOf(TextFieldValue())
}
val (password, setPassword) = rememberSaveable(stateSaver = TextFieldValueSaver) {
mutableStateOf(TextFieldValue())
}
val focusManager = LocalFocusManager.current
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
OutlinedTextField(
host,
setHost,
singleLine = true,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
label = { Text("Hostname") },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
)
OutlinedTextField(
port,
setPort,
singleLine = true,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
label = { Text("Port") },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
)
OutlinedTextField(
username,
setUsername,
singleLine = true,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
label = { Text("Username") },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
)
PasswordTextField(
password,
setPassword,
singleLine = true,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
label = { Text("Password") },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
focusManager.moveFocus(FocusDirection.Down)
}),
)
Button(
modifier = Modifier.padding(16.dp),
onClick = {
onLogin(
ConnectionData(
InetSocketAddress.createUnresolved(host.text, port.text.toIntOrNull() ?: 4242),
username.text,
password.text
)
)
}
) {
Text("Login")
}
}
}
package de.justjanne.quasseldroid.ui
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.TextFieldColors
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import de.justjanne.quasseldroid.R
@Composable
fun PasswordTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Password
),
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) {
val (showPassword, setShowPassword) = remember { mutableStateOf(false) }
val painter = painterResource(
if (showPassword) R.drawable.ic_eye_off
else R.drawable.ic_eye
)
OutlinedTextField(
value,
onValueChange,
modifier,
enabled,
readOnly,
textStyle,
label,
placeholder,
leadingIcon,
{
IconButton(onClick = { setShowPassword(!showPassword) }) {
Icon(painter = painter, contentDescription = "")
}
trailingIcon?.invoke()
},
isError,
if (showPassword) VisualTransformation.None
else PasswordVisualTransformation(),
keyboardOptions,
keyboardActions,
singleLine,
maxLines,
interactionSource,
shape,
colors
)
}
@Composable
fun PasswordTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Password
),
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) {
val (showPassword, setShowPassword) = remember { mutableStateOf(false) }
val painter = painterResource(
if (showPassword) R.drawable.ic_eye_off
else R.drawable.ic_eye
)
OutlinedTextField(
value,
onValueChange,
modifier,
enabled,
readOnly,
textStyle,
label,
placeholder,
leadingIcon,
{
IconButton(onClick = { setShowPassword(!showPassword) }) {
Icon(painter = painter, contentDescription = "")
}
trailingIcon?.invoke()
},
isError,
if (showPassword) VisualTransformation.None
else PasswordVisualTransformation(),
keyboardOptions,
keyboardActions,
singleLine,
maxLines,
interactionSource,
shape,
colors
)
}
package de.justjanne.quasseldroid.ui.theme
import androidx.compose.ui.graphics.Color
val Primary = Color(0xFF0a70c0)
val PrimaryDark = Color(0xFF105a94)
val Accent = Color(0xFFffaf3b)
val Secure = Color(0xFF4CAF50)
val PartiallySecure = Color(0xFFFFC107)
val Insecure = Color(0xFFD32F2F)
package de.justjanne.quasseldroid.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
package de.justjanne.quasseldroid.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Primary,
primaryVariant = PrimaryDark,
secondary = Accent
)
private val LightColorPalette = lightColors(
primary = Primary,
primaryVariant = PrimaryDark,
secondary = Accent
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun QuasseldroidTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
package de.justjanne.quasseldroid.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)
package de.justjanne.quasseldroid.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.transform
inline fun <T, R> Flow<T?>.mapNullable(crossinline transform: suspend (value: T) -> R): Flow<R?> =
transform { value ->
emit(value?.let { transform(it) })
}
inline fun <T, R> Flow<T?>.flatMapLatestNullable(crossinline transform: suspend (value: T) -> Flow<R>): Flow<R?> =
transform { value ->
if (value == null) emit(null)
else emitAll(transform(value))
}
@Composable
inline fun <T> rememberFlow(initial: T, calculation: @DisallowComposableCalls () -> Flow<T>): T {
return remember(calculation).collectAsState(initial).value
}
package de.justjanne.quasseldroid.util
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.ui.text.input.TextFieldValue
object TextFieldValueSaver : Saver<TextFieldValue, String> {
override fun restore(value: String) = TextFieldValue(value)
override fun SaverScope.save(value: TextFieldValue) = value.text
}
package de.justjanne.quasseldroid.util.lifecycle
import android.content.Context
interface ContextualLifecycleObserver {
fun onCreate(owner: Context) = Unit
fun onStart(owner: Context) = Unit
fun onResume(owner: Context) = Unit
fun onPause(owner: Context) = Unit
fun onStop(owner: Context) = Unit
fun onDestroy(owner: Context) = Unit
}
package de.justjanne.quasseldroid.util.lifecycle
import android.content.Context
import androidx.annotation.CallSuper
import java.util.concurrent.atomic.AtomicReference
abstract class DefaultContextualLifecycleObserver : ContextualLifecycleObserver {
private var statusInternal = AtomicReference(LifecycleStatus.DESTROYED)
protected val status: LifecycleStatus
get() = statusInternal.get()
@CallSuper
override fun onCreate(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.DESTROYED, LifecycleStatus.CREATED)) {
"Unexpected lifecycle status: onCreate called, but status is not DESTROYED"
}
}
@CallSuper
override fun onStart(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.STARTED)) {
"Unexpected lifecycle status: onStart called, but status is not CREATED"
}
}
@CallSuper
override fun onResume(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.RESUMED)) {
"Unexpected lifecycle status: onResume called, but status is not STARTED"
}
}
@CallSuper
override fun onPause(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.RESUMED, LifecycleStatus.STARTED)) {
"Unexpected lifecycle status: onPause called, but status is not RESUMED"
}
}
@CallSuper
override fun onStop(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.STARTED, LifecycleStatus.CREATED)) {
"Unexpected lifecycle status: onStop called, but status is not RESUMED"
}
}
@CallSuper
override fun onDestroy(owner: Context) {
require(statusInternal.compareAndSet(LifecycleStatus.CREATED, LifecycleStatus.DESTROYED)) {
"Unexpected lifecycle status: onDestroy called, but status is not RESUMED"
}
}
}
package de.justjanne.quasseldroid.util.lifecycle
enum class LifecycleStatus {
DESTROYED,
CREATED,
STARTED,
RESUMED
}
app/src/main/res/drawable-nodpi/profile_picture.png

277 KiB

<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="@color/colorIconDark"
android:startColor="@color/colorIconLight"
android:type="linear" />
</shape>
<?xml version="1.0" encoding="utf-8"?><!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimaryDark" />
<foreground android:drawable="@drawable/ic_shortcut_channel_foreground" />
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?><!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimaryDark" />
<foreground android:drawable="@drawable/ic_shortcut_query_foreground" />
</adaptive-icon>
<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
</vector>
<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M2,3H22C23.05,3 24,3.95 24,5V19C24,20.05 23.05,21 22,21H2C0.95,21 0,20.05 0,19V5C0,3.95 0.95,3 2,3M14,6V7H22V6H14M14,8V9H21.5L22,9V8H14M14,10V11H21V10H14M8,13.91C6,13.91 2,15 2,17V18H14V17C14,15 10,13.91 8,13.91M8,6A3,3 0 0,0 5,9A3,3 0 0,0 8,12A3,3 0 0,0 11,9A3,3 0 0,0 8,6Z" />
</vector>
<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M15,14C12.33,14 7,15.33 7,18V20H23V18C23,15.33 17.67,14 15,14M1,10V12H9V10M15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12Z" />
</vector>
<!--
Quasseldroid - Quassel client for Android
Copyright (c) 2020 Janne Mareike Koschinski
Copyright (c) 2020 The Quassel Project
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License version 3 as published
by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z" />
</vector>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment