chat ui
This commit is contained in:
+19
-25
@@ -1,31 +1,40 @@
|
||||
package ru.shadowsparky.chat.backend.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.annotation.Factory
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.chat.backend.domain.ChatService
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
import java.util.UUID
|
||||
|
||||
@Factory
|
||||
@Single
|
||||
class GigaChatService(
|
||||
private val tokenManager: GigaChatTokenManager,
|
||||
private val json: Json
|
||||
private val httpClientFactory: HttpClientFactory
|
||||
) : ChatService {
|
||||
private val httpClient by lazy { createGigaChatHttpClient() }
|
||||
private val httpClient by lazy {
|
||||
httpClientFactory.create(
|
||||
BearerAuthProvider(
|
||||
refreshTokens = {
|
||||
val newToken = tokenManager.forceRefreshToken()
|
||||
BearerTokens(newToken, null)
|
||||
},
|
||||
loadTokens = {
|
||||
BearerTokens(tokenManager.getToken(), null)
|
||||
},
|
||||
realm = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCompletion(userId: Long, messages: List<Message>): String {
|
||||
val clientId = "vbox-$userId"
|
||||
@@ -44,21 +53,6 @@ class GigaChatService(
|
||||
?: error("unexpected response $response")
|
||||
}
|
||||
|
||||
private fun createGigaChatHttpClient(): HttpClient {
|
||||
return HttpClient {
|
||||
install(ContentNegotiation) { json(json) }
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens { BearerTokens(tokenManager.getToken(), "") }
|
||||
refreshTokens {
|
||||
val newToken = tokenManager.forceRefreshToken()
|
||||
BearerTokens(newToken, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class GigaChatRequest(
|
||||
val model: String = "GigaChat-2",
|
||||
|
||||
+27
-8
@@ -1,13 +1,13 @@
|
||||
package ru.shadowsparky.chat.backend.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.Parameters
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -17,16 +17,35 @@ import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.backend.data.EnvFetcher
|
||||
import ru.shadowsparky.http.data.RefreshTokenRunner
|
||||
import ru.shadowsparky.http.domain.BadRequestException
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
import java.util.UUID
|
||||
|
||||
@Single
|
||||
class GigaChatTokenManager(
|
||||
private val authClient: HttpClient,
|
||||
private val json: Json,
|
||||
private val envFetcher: EnvFetcher
|
||||
private val envFetcher: EnvFetcher,
|
||||
private val httpClientFactory: HttpClientFactory,
|
||||
private val refreshTokenRunner: RefreshTokenRunner
|
||||
) {
|
||||
private val scope: String = "GIGACHAT_API_PERS"
|
||||
|
||||
private val authClient by lazy {
|
||||
httpClientFactory.create(
|
||||
BearerAuthProvider(
|
||||
refreshTokens = { null },
|
||||
loadTokens = {
|
||||
BearerTokens(
|
||||
envFetcher.get("GIGA_CHAT_AUTH_TOKEN").ifEmpty { error("auth token not provided") },
|
||||
null
|
||||
)
|
||||
},
|
||||
realm = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var cachedToken: String? = null
|
||||
|
||||
@@ -34,14 +53,14 @@ class GigaChatTokenManager(
|
||||
cachedToken ?: fetchNewToken()
|
||||
}
|
||||
|
||||
suspend fun forceRefreshToken(): String = mutex.withLock {
|
||||
fetchNewToken()
|
||||
suspend fun forceRefreshToken(): String {
|
||||
return refreshTokenRunner.runOrJoin {
|
||||
BearerTokens(fetchNewToken(), null)
|
||||
}?.accessToken ?: throw BadRequestException("Unable to refresh access token")
|
||||
}
|
||||
|
||||
private suspend fun fetchNewToken(): String {
|
||||
val auth = envFetcher.get("GIGA_CHAT_AUTH_TOKEN").ifEmpty { error("auth token not provided") }
|
||||
val responseString: String = authClient.post("https://ngw.devices.sberbank.ru:9443/api/v2/oauth") {
|
||||
header(HttpHeaders.Authorization, auth)
|
||||
header("RqUID", UUID.randomUUID().toString())
|
||||
contentType(ContentType.Application.FormUrlEncoded)
|
||||
setBody(FormDataContent(Parameters.build {
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@ package ru.shadowsparky.chat.backend.domain
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
|
||||
interface MessageStorage {
|
||||
suspend fun query(userId: Long, limit: Int): List<Message>
|
||||
suspend fun query(userId: Long, afterId: Long?, limit: Int): List<Message>
|
||||
suspend fun insert(userId: Long, message: Message): Long
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ class ProcessUserMessageUseCase(
|
||||
private val chatService: ChatService
|
||||
) {
|
||||
suspend fun execute(userId: Long): Message {
|
||||
val history = storage.query(userId, limit = 10).toList()
|
||||
val history = storage.query(userId, null, limit = 10).toList()
|
||||
val sortedHistory = history.sortedBy { it.timestamp }
|
||||
val systemMessage = Message(
|
||||
role = ChatRoles.SYSTEM,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="chat_title">Чат</string>
|
||||
<string name="chat_load_error">Не удалось загрузить сообщения</string>
|
||||
<string name="chat_send_error">Не удалось отправить сообщение</string>
|
||||
<string name="chat_retry_button">Повторить</string>
|
||||
<string name="chat_input_placeholder">Сообщение...</string>
|
||||
<string name="chat_send_button">Отправить</string>
|
||||
<string name="chat_ok">ОК</string>
|
||||
</resources>
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package ru.shadowsparky.chat.presentation
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.value.MutableValue
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.shadowsparky.chat.domain.ChatRepository
|
||||
import ru.shadowsparky.chat.domain.ChatRoles
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
|
||||
sealed interface ChatState {
|
||||
object Loading : ChatState
|
||||
|
||||
data class Error(val message: String) : ChatState
|
||||
|
||||
data class Data(
|
||||
val messages: List<Message>,
|
||||
val isSending: Boolean = false,
|
||||
val isSubsequentLoading: Boolean = false,
|
||||
val sendError: String? = null
|
||||
) : ChatState
|
||||
}
|
||||
|
||||
class ChatComponent(
|
||||
componentContext: ComponentContext,
|
||||
private val repository: ChatRepository
|
||||
) : ComponentContext by componentContext {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()).also { ctx ->
|
||||
lifecycle.doOnDestroy { ctx.cancel() }
|
||||
}
|
||||
|
||||
private val _state = MutableValue<ChatState>(ChatState.Loading)
|
||||
val state: Value<ChatState> = _state
|
||||
|
||||
init {
|
||||
loadInitialMessages()
|
||||
}
|
||||
|
||||
fun loadInitialMessages() {
|
||||
_state.value = ChatState.Loading
|
||||
scope.launch {
|
||||
try {
|
||||
val dbList = repository.query(afterId = null)
|
||||
_state.value = ChatState.Data(messages = dbList)
|
||||
} catch (e: Exception) {
|
||||
_state.value = ChatState.Error(e.message ?: e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreMessages() {
|
||||
val currentState = _state.value as? ChatState.Data ?: return
|
||||
if (currentState.isSubsequentLoading || currentState.messages.isEmpty()) return
|
||||
val latestMessageId = currentState.messages.last().id ?: return
|
||||
_state.value = currentState.copy(isSubsequentLoading = true)
|
||||
scope.launch {
|
||||
try {
|
||||
val newMessages = repository.query(afterId = latestMessageId)
|
||||
if (newMessages.isNotEmpty()) {
|
||||
_state.value = currentState.copy(
|
||||
messages = currentState.messages + newMessages,
|
||||
isSubsequentLoading = false
|
||||
)
|
||||
} else {
|
||||
_state.value = currentState.copy(isSubsequentLoading = false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value = currentState.copy(isSubsequentLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(content: String) {
|
||||
val currentState = _state.value as? ChatState.Data ?: return
|
||||
if (content.isBlank() || currentState.isSending) return
|
||||
_state.value = currentState.copy(isSending = true, sendError = null)
|
||||
scope.launch {
|
||||
try {
|
||||
repository.send(Message(role = ChatRoles.USER, content = content, timestamp = 0L))
|
||||
val latestMessageId = currentState.messages.lastOrNull()?.id
|
||||
val freshMessages = repository.query(afterId = latestMessageId)
|
||||
_state.value = currentState.copy(
|
||||
messages = currentState.messages + freshMessages,
|
||||
isSending = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.value = currentState.copy(
|
||||
isSending = false,
|
||||
sendError = e.message ?: e.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSendError() {
|
||||
val currentState = _state.value as? ChatState.Data ?: return
|
||||
_state.value = currentState.copy(sendError = null)
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package ru.shadowsparky.chat.presentation
|
||||
|
||||
import androiddev.feature.chat.chat_client.generated.resources.Res
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_input_placeholder
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_load_error
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_ok
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_retry_button
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_send_button
|
||||
import androiddev.feature.chat.chat_client.generated.resources.chat_title
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.shadowsparky.chat.domain.ChatRoles
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
import ru.shadowsparky.ui.AppScaffold
|
||||
import ru.shadowsparky.ui.BlurState
|
||||
import ru.shadowsparky.ui.applyBlurEffect
|
||||
import ru.shadowsparky.ui.applyBlurSource
|
||||
import ru.shadowsparky.ui.components.LazyColumn
|
||||
import ru.shadowsparky.ui.components.Loading
|
||||
import ru.shadowsparky.ui.rememberBlurState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatScreen(component: ChatComponent, modifier: Modifier = Modifier) {
|
||||
val state by component.state.subscribeAsState()
|
||||
val blur = rememberBlurState()
|
||||
AppScaffold(
|
||||
appBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(Res.string.chat_title)) },
|
||||
modifier = Modifier.applyBlurEffect(blur)
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (val currentState = state) {
|
||||
is ChatState.Loading -> {
|
||||
Loading(Modifier.padding(paddingValues).fillMaxSize())
|
||||
}
|
||||
is ChatState.Error -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
Text(text = stringResource(Res.string.chat_load_error), color = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = { component.loadInitialMessages() }) {
|
||||
Text(stringResource(Res.string.chat_retry_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatState.Data -> {
|
||||
ChatContent(
|
||||
paddingValues,
|
||||
blur,
|
||||
state = currentState,
|
||||
onLoadMore = { component.loadMoreMessages() },
|
||||
onSendMessage = { text -> component.sendMessage(text) },
|
||||
onDismissError = { component.clearSendError() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatContent(
|
||||
paddingValues: PaddingValues,
|
||||
blurState: BlurState?,
|
||||
state: ChatState.Data,
|
||||
onLoadMore: () -> Unit,
|
||||
onSendMessage: (String) -> Unit,
|
||||
onDismissError: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
var textInput by remember { mutableStateOf("") }
|
||||
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val totalItems = layoutInfo.totalItemsCount
|
||||
val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
|
||||
totalItems > 0 && lastVisibleItemIndex >= totalItems - 2
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
if (shouldLoadMore) {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.messages.size) {
|
||||
if (state.messages.isNotEmpty()) {
|
||||
listState.animateScrollToItem(state.messages.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.applyBlurSource(blurState)
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(items = state.messages, key = { it.id ?: it.hashCode() }) { message ->
|
||||
MessageBubble(message = message)
|
||||
}
|
||||
|
||||
if (state.isSubsequentLoading) {
|
||||
item {
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(8.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.sendError?.let { error ->
|
||||
Snackbar(
|
||||
action = {
|
||||
TextButton(onClick = onDismissError) { Text(stringResource(Res.string.chat_ok)) }
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(error)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextField(
|
||||
value = textInput,
|
||||
onValueChange = { textInput = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text(stringResource(Res.string.chat_input_placeholder)) },
|
||||
enabled = !state.isSending
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
if (textInput.isNotBlank()) {
|
||||
onSendMessage(textInput)
|
||||
textInput = ""
|
||||
}
|
||||
},
|
||||
enabled = textInput.isNotBlank() && !state.isSending
|
||||
) {
|
||||
if (state.isSending) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Text(stringResource(Res.string.chat_send_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBubble(message: Message) {
|
||||
val isUser = message.role == ChatRoles.USER
|
||||
val alignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val containerColor = if (isUser) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = alignment) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||
modifier = Modifier.widthIn(max = 280.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(text = message.content, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -1,6 +1,17 @@
|
||||
package ru.shadowsparky.chat.domain
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface ChatRepository {
|
||||
suspend fun query(): List<Message>
|
||||
suspend fun query(afterId: Long?): List<Message>
|
||||
suspend fun send(message: Message)
|
||||
|
||||
companion object {
|
||||
const val ARG_AFTER_ID = "after_id"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MessagesResponse(
|
||||
val content: List<Message>
|
||||
)
|
||||
|
||||
+17
-17
@@ -1,22 +1,22 @@
|
||||
package ru.shadowsparky.updater.presentation
|
||||
|
||||
import androiddev.libs.updater.updater_client.generated.resources.Res
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_close
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_download
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_grant
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_install
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_retry
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_empty
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_title
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_desc_permission
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_download_progress
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_available
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_completed
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_downloading
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_error
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_not_found
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
|
||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
|
||||
import androiddev.feature.updater.updater_client.generated.resources.Res
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_btn_close
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_btn_download
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_btn_grant
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_btn_install
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_btn_retry
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_changelog_empty
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_changelog_title
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_desc_permission
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_download_progress
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_available
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_completed
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_downloading
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_error
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_not_found
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_permission
|
||||
import androiddev.feature.updater.updater_client.generated.resources.updater_title_ready
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package ru.shadowsparky.videoplayer
|
||||
|
||||
import android.content.Context
|
||||
import androiddev.libs.video_player.generated.resources.Res
|
||||
import androiddev.libs.video_player.generated.resources.player_audio_track
|
||||
import androiddev.feature.video_player.generated.resources.Res
|
||||
import androiddev.feature.video_player.generated.resources.player_audio_track
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
package ru.shadowsparky.videoplayer.ui
|
||||
|
||||
import androiddev.libs.video_player.generated.resources.Res
|
||||
import androiddev.libs.video_player.generated.resources.lock_24px
|
||||
import androiddev.libs.video_player.generated.resources.lock_open_24px
|
||||
import androiddev.libs.video_player.generated.resources.music_note_24px
|
||||
import androiddev.libs.video_player.generated.resources.pause_24px
|
||||
import androiddev.libs.video_player.generated.resources.play_arrow_24px
|
||||
import androiddev.libs.video_player.generated.resources.replay_24px
|
||||
import androiddev.feature.video_player.generated.resources.Res
|
||||
import androiddev.feature.video_player.generated.resources.lock_24px
|
||||
import androiddev.feature.video_player.generated.resources.lock_open_24px
|
||||
import androiddev.feature.video_player.generated.resources.music_note_24px
|
||||
import androiddev.feature.video_player.generated.resources.pause_24px
|
||||
import androiddev.feature.video_player.generated.resources.play_arrow_24px
|
||||
import androiddev.feature.video_player.generated.resources.replay_24px
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.focusGroup
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package ru.shadowsparky.videoplayer.ui
|
||||
|
||||
import androiddev.libs.video_player.generated.resources.Res
|
||||
import androiddev.libs.video_player.generated.resources.player_error_summary
|
||||
import androiddev.libs.video_player.generated.resources.player_error_title
|
||||
import androiddev.libs.video_player.generated.resources.player_retry
|
||||
import androiddev.feature.video_player.generated.resources.Res
|
||||
import androiddev.feature.video_player.generated.resources.player_error_summary
|
||||
import androiddev.feature.video_player.generated.resources.player_error_title
|
||||
import androiddev.feature.video_player.generated.resources.player_retry
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
||||
Reference in New Issue
Block a user