impl chat screen in giga wear
This commit is contained in:
@@ -25,6 +25,6 @@ fun Application.module() {
|
|||||||
install(ContentNegotiation) { json(koin.get()) }
|
install(ContentNegotiation) { json(koin.get()) }
|
||||||
routing {
|
routing {
|
||||||
get("health") { this.call.respond(HttpStatusCode.OK) }
|
get("health") { this.call.respond(HttpStatusCode.OK) }
|
||||||
setupChat(koin.get(), koin.get())
|
setupChat(koin.get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ class MessageStorageImpl : MessageStorage {
|
|||||||
} else {
|
} else {
|
||||||
userMessages
|
userMessages
|
||||||
}
|
}
|
||||||
return filteredList.take(limit).toList()
|
return filteredList.reversed().take(limit).sortedBy { it.timestamp }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import ru.shadowsparky.ui.nav.RootComponent
|
|||||||
class GigaWearRootComponent(
|
class GigaWearRootComponent(
|
||||||
context: ComponentContext
|
context: ComponentContext
|
||||||
) : RootComponent<Route>(context) {
|
) : RootComponent<Route>(context) {
|
||||||
override val initStack: List<Route> = listOf(Route.MainScreen())
|
override val initStack: List<Route> = listOf(Route.ChatScreen())
|
||||||
override val serializer: KSerializer<Route> = Route.serializer()
|
override val serializer: KSerializer<Route> = Route.serializer()
|
||||||
override val koin: Koin = ru.shadowsparky.koin
|
override val koin: Koin = ru.shadowsparky.koin
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import com.arkivanov.decompose.retainedComponent
|
import com.arkivanov.decompose.retainedComponent
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import ru.shadowsparky.gigawear.presentation.main.MainScreen
|
import ru.shadowsparky.gigawear.presentation.chat.ChatScreen
|
||||||
import ru.shadowsparky.gigawear.presentation.nav.Child
|
import ru.shadowsparky.gigawear.presentation.nav.Child
|
||||||
import ru.shadowsparky.ui.nav.Nav
|
import ru.shadowsparky.ui.nav.Nav
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
Nav(root) { child, _ ->
|
Nav(root) { child, _ ->
|
||||||
when (child) {
|
when (child) {
|
||||||
is Child.MainChild -> MainScreen(child.component)
|
is Child.MainChild -> ChatScreen(child.component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package ru.shadowsparky.gigawear.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.wear.compose.material3.MaterialTheme
|
||||||
|
import androidx.wear.compose.material3.Text
|
||||||
|
import ru.shadowsparky.gigawear.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WearErrorContent(
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.error_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package ru.shadowsparky.gigawear.presentation.chat
|
||||||
|
|
||||||
|
import com.arkivanov.decompose.ComponentContext
|
||||||
|
import com.arkivanov.decompose.value.MutableValue
|
||||||
|
import com.arkivanov.decompose.value.Value
|
||||||
|
import com.arkivanov.decompose.value.update
|
||||||
|
import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
|
import org.koin.core.annotation.Named
|
||||||
|
import ru.shadowsparky.chat.domain.ChatRepository
|
||||||
|
import ru.shadowsparky.chat.domain.ChatRoles
|
||||||
|
import ru.shadowsparky.chat.domain.Message
|
||||||
|
import ru.shadowsparky.gigawear.presentation.nav.Child
|
||||||
|
import ru.shadowsparky.gigawear.presentation.nav.Route
|
||||||
|
import ru.shadowsparky.ui.nav.ComponentFactory
|
||||||
|
import ru.shadowsparky.ui.nav.RouteNavigator
|
||||||
|
|
||||||
|
class ChatComponent(
|
||||||
|
private val context: ComponentContext,
|
||||||
|
private val repository: ChatRepository
|
||||||
|
) : ComponentContext by context {
|
||||||
|
private val scope = coroutineScope()
|
||||||
|
|
||||||
|
private val _state = MutableValue(ChatUiState())
|
||||||
|
val state: Value<ChatUiState> = _state
|
||||||
|
|
||||||
|
private val _events = Channel<ComponentEvent>(Channel.BUFFERED)
|
||||||
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
|
fun onMicClicked() {
|
||||||
|
if (_state.value.isLoading) return
|
||||||
|
scope.launch {
|
||||||
|
_events.send(ComponentEvent.RequestSpeechRecognition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSpeechRecognized(text: String) {
|
||||||
|
_state.value = _state.value.copy(userText = text, isLoading = true, error = null)
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val userMessage = Message(
|
||||||
|
role = ChatRoles.USER,
|
||||||
|
content = text,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
val responseMessage = repository.send(userMessage)
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
assistantText = responseMessage.content,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
_events.send(ComponentEvent.RequestSpeak(responseMessage.content))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(error = e, isLoading = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ComponentEvent {
|
||||||
|
object RequestSpeechRecognition : ComponentEvent
|
||||||
|
data class RequestSpeak(val text: String) : ComponentEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChatUiState(
|
||||||
|
val userText: String? = null,
|
||||||
|
val assistantText: String? = null,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: Throwable? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
@Named(Route.CHAT)
|
||||||
|
class ChatComponentFactory(
|
||||||
|
private val chatRepository: ChatRepository
|
||||||
|
) : ComponentFactory<Child, Route.ChatScreen, Route> {
|
||||||
|
override fun create(
|
||||||
|
route: Route.ChatScreen,
|
||||||
|
child: ComponentContext,
|
||||||
|
root: RouteNavigator<Route>
|
||||||
|
): Child {
|
||||||
|
val component = ChatComponent(child, chatRepository)
|
||||||
|
return Child.MainChild(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
package ru.shadowsparky.gigawear.presentation.chat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.speech.RecognizerIntent
|
||||||
|
import android.speech.tts.TextToSpeech
|
||||||
|
import android.speech.tts.UtteranceProgressListener
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.wear.compose.foundation.ScrollInfoProvider
|
||||||
|
import androidx.wear.compose.material3.AppScaffold
|
||||||
|
import androidx.wear.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.wear.compose.material3.EdgeButton
|
||||||
|
import androidx.wear.compose.material3.EdgeButtonSize
|
||||||
|
import androidx.wear.compose.material3.Icon
|
||||||
|
import androidx.wear.compose.material3.MaterialTheme
|
||||||
|
import androidx.wear.compose.material3.ScreenScaffold
|
||||||
|
import androidx.wear.compose.material3.ScrollIndicator
|
||||||
|
import androidx.wear.compose.material3.Text
|
||||||
|
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import ru.shadowsparky.gigawear.R
|
||||||
|
import ru.shadowsparky.gigawear.presentation.WearErrorContent
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatScreen(component: ChatComponent) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val state by component.state.subscribeAsState()
|
||||||
|
|
||||||
|
var tts by remember {
|
||||||
|
mutableStateOf<TextToSpeech?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSpeaking by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(context) {
|
||||||
|
val ttsInstance = TextToSpeech(context) { status ->
|
||||||
|
if (status == TextToSpeech.SUCCESS) {
|
||||||
|
tts?.language = Locale.getDefault()
|
||||||
|
tts?.setOnUtteranceProgressListener(
|
||||||
|
object : UtteranceProgressListener() {
|
||||||
|
override fun onStart(utteranceId: String?) { isSpeaking = true }
|
||||||
|
override fun onDone(utteranceId: String?) { isSpeaking = false }
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onError(utteranceId: String?) { isSpeaking = false }
|
||||||
|
override fun onError(utteranceId: String?, errorCode: Int) {
|
||||||
|
super.onError(utteranceId, errorCode)
|
||||||
|
isSpeaking = false
|
||||||
|
}
|
||||||
|
override fun onStop(
|
||||||
|
utteranceId: String?,
|
||||||
|
interrupted: Boolean
|
||||||
|
) { isSpeaking = false }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tts = ttsInstance
|
||||||
|
onDispose {
|
||||||
|
ttsInstance.stop()
|
||||||
|
ttsInstance.shutdown()
|
||||||
|
tts = null
|
||||||
|
isSpeaking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val speechLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) {
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val spokenText = result.data
|
||||||
|
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||||
|
?.firstOrNull()
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
if (spokenText.isNotEmpty()) {
|
||||||
|
component.onSpeechRecognized(spokenText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(component) {
|
||||||
|
component.events.collectLatest { event ->
|
||||||
|
when (event) {
|
||||||
|
is ChatComponent.ComponentEvent.RequestSpeechRecognition -> {
|
||||||
|
val intent = Intent(
|
||||||
|
RecognizerIntent.ACTION_RECOGNIZE_SPEECH
|
||||||
|
).apply {
|
||||||
|
putExtra(
|
||||||
|
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||||
|
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
RecognizerIntent.EXTRA_LANGUAGE,
|
||||||
|
Locale.getDefault()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
speechLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChatComponent.ComponentEvent.RequestSpeak -> {
|
||||||
|
tts?.speak(
|
||||||
|
event.text,
|
||||||
|
TextToSpeech.QUEUE_FLUSH,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val scrollInfoProvider = remember(listState) {
|
||||||
|
ScrollInfoProvider(listState)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppScaffold {
|
||||||
|
ScreenScaffold(
|
||||||
|
scrollInfoProvider = scrollInfoProvider,
|
||||||
|
scrollIndicator = { ScrollIndicator(listState) },
|
||||||
|
edgeButton = {
|
||||||
|
EdgeButton(
|
||||||
|
onClick = {
|
||||||
|
if (isSpeaking) {
|
||||||
|
tts?.stop()
|
||||||
|
isSpeaking = false
|
||||||
|
} else {
|
||||||
|
component.onMicClicked()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isSpeaking || !state.isLoading,
|
||||||
|
buttonSize = EdgeButtonSize.Small
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
if (isSpeaking) {
|
||||||
|
R.drawable.cancel_24px
|
||||||
|
} else {
|
||||||
|
R.drawable.mic_24px
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { contentPadding ->
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
val text = state.userText ?: stringResource(R.string.input_text_to_assist)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.assistantText?.let {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.padding(horizontal = 20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
item {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
item {
|
||||||
|
WearErrorContent(it.message ?: it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
package ru.shadowsparky.gigawear.presentation.main
|
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext
|
|
||||||
import org.koin.core.annotation.Factory
|
|
||||||
import org.koin.core.annotation.Named
|
|
||||||
import ru.shadowsparky.gigawear.presentation.nav.Child
|
|
||||||
import ru.shadowsparky.gigawear.presentation.nav.Route
|
|
||||||
import ru.shadowsparky.ui.nav.ComponentFactory
|
|
||||||
import ru.shadowsparky.ui.nav.RouteNavigator
|
|
||||||
|
|
||||||
class MainComponent(
|
|
||||||
private val context: ComponentContext
|
|
||||||
) : ComponentContext by context {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Factory
|
|
||||||
@Named(Route.MAIN)
|
|
||||||
class MainComponentFactory : ComponentFactory<Child, Route.MainScreen, Route> {
|
|
||||||
override fun create(
|
|
||||||
route: Route.MainScreen,
|
|
||||||
child: ComponentContext,
|
|
||||||
root: RouteNavigator<Route>
|
|
||||||
): Child {
|
|
||||||
val component = MainComponent(child)
|
|
||||||
return Child.MainChild(component)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package ru.shadowsparky.gigawear.presentation.main
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.safeContentPadding
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MainScreen(component: MainComponent) {
|
|
||||||
Box(modifier = Modifier.safeContentPadding()) {
|
|
||||||
Text("Тут пока что ничего нет. Совсем ничего. Приложение в активной разработке")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
package ru.shadowsparky.gigawear.presentation.nav
|
package ru.shadowsparky.gigawear.presentation.nav
|
||||||
|
|
||||||
import ru.shadowsparky.gigawear.presentation.main.MainComponent
|
import ru.shadowsparky.gigawear.presentation.chat.ChatComponent
|
||||||
|
|
||||||
sealed class Child : ru.shadowsparky.ui.nav.Child {
|
sealed class Child : ru.shadowsparky.ui.nav.Child {
|
||||||
class MainChild(val component: MainComponent) : Child()
|
class MainChild(val component: ChatComponent) : Child()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,9 +6,9 @@ import kotlinx.serialization.Serializable
|
|||||||
sealed class Route : ru.shadowsparky.ui.nav.Route() {
|
sealed class Route : ru.shadowsparky.ui.nav.Route() {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MainScreen(override val routeId: String = MAIN) : Route()
|
data class ChatScreen(override val routeId: String = CHAT) : Route()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAIN = "main"
|
const val CHAT = "chat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M336,680L480,536L624,680L680,624L536,480L680,336L624,280L480,424L336,280L280,336L424,480L280,624L336,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M395,525Q360,490 360,440L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,490 565,525Q530,560 480,560Q430,560 395,525ZM480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 338.5,581.5Q397,640 480,640Q563,640 621.5,581.5Q680,523 680,440L760,440Q760,545 692,624Q624,703 520,717L520,840L440,840ZM508.5,468.5Q520,457 520,440L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,440Q440,457 451.5,468.5Q463,480 480,480Q497,480 508.5,468.5Z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">GigaWear</string>
|
<string name="app_name">Свин Wear</string>
|
||||||
<string name="hello_world">Hello, %1$s!</string>
|
<string name="input_text_to_assist">Введите запрос к ассистенту</string>
|
||||||
<string name="tile_label">Example tile</string>
|
<string name="error_title">Произошла ошибка</string>
|
||||||
<string name="complication_label">Example complication</string>
|
</resources>
|
||||||
</resources>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user