impl chat user interface

This commit is contained in:
2026-07-29 18:52:20 +03:00
parent 725c78aa2d
commit ab14a86305
15 changed files with 265 additions and 167 deletions
@@ -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="#fff"
android:pathData="M668.5,428.5Q680,417 680,400Q680,383 668.5,371.5Q657,360 640,360Q623,360 611.5,371.5Q600,383 600,400Q600,417 611.5,428.5Q623,440 640,440Q657,440 668.5,428.5ZM320,360L520,360L520,280L320,280L320,360ZM180,840Q146,726 113,612.5Q80,499 80,380Q80,288 144,224Q208,160 300,160L500,160Q529,122 570.5,101Q612,80 660,80Q685,80 702.5,97.5Q720,115 720,140Q720,146 718.5,152Q717,158 715,163Q711,174 707.5,185.5Q704,197 702,209L793,300L880,300L880,579L767,616L700,840L480,840L480,760L400,760L400,840L180,840ZM240,760L320,760L320,680L560,680L560,760L640,760L702,554L800,521L800,380L760,380L620,240Q620,220 622.5,201.5Q625,183 630,164Q601,172 579,191.5Q557,211 547,240L300,240Q242,240 201,281Q160,322 160,380Q160,478 187,571.5Q214,665 240,760ZM480,462Q480,462 480,462Q480,462 480,462Q480,462 480,462Q480,462 480,462L480,462Q480,462 480,462Q480,462 480,462Q480,462 480,462Q480,462 480,462L480,462L480,462L480,462L480,462L480,462L480,462L480,462L480,462L480,462Z"/>
</vector>
@@ -68,4 +68,6 @@
<string name="movie_tags_empty">Настроить теги</string>
<string name="user_tags_filter_disable">Не фильтровать</string>
<string name="check_for_updates">Поиск обновлений</string>
<string name="chat_title_app_bar">Братан СвинGPT</string>
<string name="chat_title">СвинGPT</string>
</resources>
@@ -0,0 +1,27 @@
package ru.shadowsparky.vbox.shared.data.chat
import org.koin.core.annotation.Factory
import ru.shadowsparky.chat.domain.ChatTextParser
import ru.shadowsparky.chat.domain.TextToken
@Factory
class MovieTextParser : ChatTextParser {
private val regex = Regex("\\[MOVIE\\](.*?)\\[/MOVIE\\]")
override fun parse(content: String): List<TextToken> {
val tokens = mutableListOf<TextToken>()
var lastIndex = 0
regex.findAll(content).forEach { matchResult ->
if (matchResult.range.first > lastIndex) {
tokens.add(TextToken.Plain(content.substring(lastIndex, matchResult.range.first)))
}
val movieTitle = matchResult.groupValues[1]
tokens.add(TextToken.Link(text = movieTitle))
lastIndex = matchResult.range.last + 1
}
if (lastIndex < content.length) {
tokens.add(TextToken.Plain(content.substring(lastIndex)))
}
return tokens.ifEmpty { listOf(TextToken.Plain(content)) }
}
}
@@ -1,4 +1,4 @@
package ru.shadowsparky.vbox.shared.data
package ru.shadowsparky.vbox.shared.data.chat
import io.ktor.client.HttpClient
import io.ktor.client.call.body
@@ -8,6 +8,8 @@ import org.koin.core.annotation.Factory
import ru.shadowsparky.chat.domain.ChatRepository
import ru.shadowsparky.chat.domain.Message
import ru.shadowsparky.chat.domain.MessagesResponse
import ru.shadowsparky.vbox.shared.data.apply
import ru.shadowsparky.vbox.shared.data.get
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
import ru.shadowsparky.vbox.shared.domain.ServerConfigurationRepository
import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
@@ -25,7 +27,7 @@ class RemoteChatRepository(
mapOf(ChatRepository.ARG_AFTER_ID to afterId.toString())
}
return httpClient.get<MessagesResponse>(
"$DYNAMIC_PREFIX/chat/query",
"${DYNAMIC_PREFIX}/chat/query",
args,
serverConfigurationRepository.getServerConfiguration()
).content
@@ -36,7 +38,7 @@ class RemoteChatRepository(
setBody(message)
serverConfigurationRepository.getServerConfiguration().apply(
this,
"$DYNAMIC_PREFIX/chat/send"
"${DYNAMIC_PREFIX}/chat/send"
)
}.body<Message>()
}
@@ -12,7 +12,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.seiko.imageloader.LocalImageLoader
import ru.shadowsparky.chat.presentation.ChatScreen
import ru.shadowsparky.koin
import ru.shadowsparky.ui.components.BackButton
import ru.shadowsparky.ui.nav.Nav
@@ -64,10 +63,10 @@ private fun NavInternal(root: VBoxRootComponent) {
is Child.EditPasswordChild -> EditPasswordScreen(child.component)
is Child.LoadingChild -> LoadingScreen(child.component)
is Child.OfflineVideoChild -> OfflineVideoScreen(child.component)
is Child.ChatChild -> ChatScreen(child.component)
is Child.VideosChild,
is Child.SearchChild,
is Child.SettingsChild,
is Child.ChatChild,
is Child.SavedMovieChild -> {
if (child is Child.VideosChild && child.component.search != null) {
VideoScreen(child.component)
@@ -1,12 +1,14 @@
package ru.shadowsparky.vbox.shared.presentation
import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.chat_title
import androiddev.apps.vbox.client.shared.generated.resources.home
import androiddev.apps.vbox.client.shared.generated.resources.recommendations
import androiddev.apps.vbox.client.shared.generated.resources.saved_movies
import androiddev.apps.vbox.client.shared.generated.resources.search
import androiddev.apps.vbox.client.shared.generated.resources.settings
import androiddev.apps.vbox.client.shared.generated.resources.star_unchecked
import androiddev.apps.vbox.client.shared.generated.resources.svingpt
import androidx.compose.runtime.Composable
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@@ -31,6 +33,11 @@ fun createNavigationList(): List<NavigationEntry<Route>> {
stringResource(Res.string.search),
painterResource(Res.drawable.search)
),
NavigationEntry(
Route.Chat(),
stringResource(Res.string.chat_title),
painterResource(Res.drawable.svingpt)
),
NavigationEntry(
Route.Settings(),
stringResource(Res.string.settings),
@@ -14,6 +14,7 @@ import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.rememberBlurState
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.presentation.chat.ChatScreen
import ru.shadowsparky.vbox.shared.presentation.list.VideoContent
import ru.shadowsparky.vbox.shared.presentation.nav.Child
import ru.shadowsparky.vbox.shared.presentation.nav.Route
@@ -29,7 +30,7 @@ fun MainScreen(route: Route, child: Child, root: VBoxRootComponent) {
child.component,
route
) { root.navReplace(it) }
is Child.ChatChild -> ChatScreen(child.component, route) { root.navReplace(it) }
else -> MainScreenInternal(route, child) { root.navReplace(it) }
}
}
@@ -4,6 +4,7 @@ import com.arkivanov.decompose.ComponentContext
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named
import ru.shadowsparky.chat.domain.ChatRepository
import ru.shadowsparky.chat.domain.ChatTextParser
import ru.shadowsparky.chat.presentation.ChatComponent
import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
@@ -14,7 +15,8 @@ import ru.shadowsparky.vbox.shared.presentation.nav.RouteIds
@Factory
@Named(RouteIds.CHAT)
class ChatComponentFactory(
private val chatRepository: ChatRepository
private val chatRepository: ChatRepository,
private val textParser: ChatTextParser
) : ComponentFactory<Child.ChatChild, Route.Chat, Route> {
override fun create(
route: Route.Chat,
@@ -23,7 +25,9 @@ class ChatComponentFactory(
): Child.ChatChild {
val comp = ChatComponent(
child,
chatRepository
chatRepository,
textParser,
onLinkClicked = { root.nav(Route.Videos(it)) }
)
return Child.ChatChild(comp)
}
@@ -0,0 +1,39 @@
package ru.shadowsparky.vbox.shared.presentation.chat
import androiddev.apps.vbox.client.shared.generated.resources.Res
import androiddev.apps.vbox.client.shared.generated.resources.chat_title_app_bar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.chat.presentation.ChatComponent
import ru.shadowsparky.chat.presentation.ChatContent
import ru.shadowsparky.ui.LocalBlurState
import ru.shadowsparky.ui.applyBlurEffect
import ru.shadowsparky.ui.theme.ComponentDefaults
import ru.shadowsparky.vbox.shared.presentation.BaseMainScreen
import ru.shadowsparky.vbox.shared.presentation.nav.Route
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatScreen(component: ChatComponent, route: Route, onNavigate: (Route) -> Unit) {
val state by component.state.subscribeAsState()
BaseMainScreen(
appBar = {
TopAppBar(
title = { Text(stringResource(Res.string.chat_title_app_bar)) },
colors = ComponentDefaults.topAppBarColors(),
modifier = Modifier.applyBlurEffect(LocalBlurState.current)
)
},
route,
onNavigate = onNavigate,
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
content = { ChatContent(component, it, state) }
)
}
@@ -37,8 +37,7 @@ class SettingsComponent(
val doRegister: () -> Unit,
val editPassword: () -> Unit,
val showOffline: () -> Unit,
val showTags: () -> Unit,
val showChat: () -> Unit
val showTags: () -> Unit
) : ComponentContext by context {
val updateComponent = updateComponentFactory.create(childContext("UpdateComponent"))
@@ -100,8 +99,7 @@ class SettingsComponentFactory(
tokenStorage = tokenStorage,
showOffline = { root.nav(Route.OfflineVideo()) },
offlineSupport = offlineStorage != null,
showTags = { root.nav(Route.Settings.TagOverview()) },
showChat = { root.nav(Route.Chat())}
showTags = { root.nav(Route.Settings.TagOverview()) }
)
return Child.SettingsChild(comp)
}
@@ -82,12 +82,6 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
)
}
}
item {
TextPreference(
text = "chat",
modifier = Modifier.clickable { comp.showChat() }
)
}
}
section {
val needAuth = tokenInfo == null